🔐 feat: OIDC Bearer Token Authentication for Remote Agent API#12450
🔐 feat: OIDC Bearer Token Authentication for Remote Agent API#12450danny-avila merged 32 commits intodanny-avila:devfrom
Conversation
|
Hello, fellas! Looking forward for your feedback, is this expected behavior or something is missing. |
@SpectralOne - cool - thank you for your effort... looks like exactly what I was asking for... I did not test it so far but looks good for me - maybe one little thing... This middleware reuses findOpenIDUser(...), but it ignores the returned migration flag and never persists the OpenID binding. In the existing OpenID auth flow, when a user is found only by email, LibreChat treats that as a migration/linking case and updates the user record with the OpenID identity (provider: 'openid', openidId: payload.sub, etc.). Here, the request is allowed through, but the binding is never saved. That means remote-agent auth can keep relying on email fallback on every request instead of completing the one-time identity link. This is weaker than the existing OpenID login path and can leave account linking permanently incomplete. I think this middleware should handle migration the same way as the existing OpenID strategy, including persisting the user update before continuing. |
|
@danny-avila - would be cool to have this soon... this would enable 2 use cases for us which quite some people have been asking for... thanks :) |
fair point, i will fix it shortly. thanks! |
|
@SpectralOne - one more point that just came up for me... I know there is also an admin api... maybe there will be more apis in future... so, would it make sense to support optional required scopes/app roles here as part of config, so deployments can distinguish token intent at the IdP level as well? |
82f1ec5 to
5232ee5
Compare
|
@peeeteeer I think this is good a proposal. But i think this should be implemented in a separate Pull Request. Config may looks like: endpoints:
agents:
remoteApi:
auth:
oidc:
enabled: true
issuer: https://auth.example.com/realms/myrealm
requiredScopes:
- remote_agent
requiredRoles:
- librechat-agent
requiredAppRoles:
- LibreChat.RemoteAgentor split it like this: # remote agent API
remoteApi:
auth:
oidc:
requiredRoles: [librechat-agent]
# future admin API
adminApi:
auth:
oidc:
requiredRoles: [librechat-admin]In any case, this functionality needs to be thoroughly considered. |
no, I meant something way simpler... when an oidc token is issued it usually contains... typically you would have aud: the librechat server as a client id I think we do not need the roles here since librechat has it's own authorization system. But I do think we need to introduce/check the scope - otherwise you would be in future be able to use the token issued to the server for all operations... so my suggestion is to add this to the config you already have.... in code this is easy to verify - just between the two lines here... |
Oh, okay. Done. Indeed way simplier. |
|
@danny-avila Hello, could you take a look at this PR, please? Not urgent, when you will have free time. |
|
@SpectralOne @danny-avila - I was just testing this successfully in our environment - works without any issues... use case was integration of an Libechat agent in another website/portal... |
|
maybe one idea... when the api key auth is disabled we also should disable the generation of api keys in the UI |
|
thanks @peeeteeer I'll be reviewing this shortly |
|
@codex review |
|
/gitnexus index |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7b36e24079
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@danny-avila Hi, I've fixed codex review errors |
|
@danny-avila @SpectralOne - today I was coming around a shortcoming of the current implementation... The current remote OIDC implementation only works for users that already exist in LibreChat. A valid Entra/OIDC Bearer token is verified by remoteAgentAuth, but if no matching LibreChat user is found, the request is rejected with 401 Unauthorized. The middleware does not auto-provision a LibreChat user from a valid token. Agent authorization is then decided by LibreChat’s existing ACL/group permission system, not directly by Entra token claims. That means the user must already be known locally before the existing agent-level access checks can run. In addition, the remote-agent OIDC path does not currently synchronize Entra group memberships into LibreChat group membership state. Group sync exists in the browser OpenID login flow, but not in remoteAgentAuth. As a result, remote OIDC access currently depends on pre-existing LibreChat user records and pre-existing synced group membership data. To support a real integration into other portals if would be required to support first-time remote OIDC users by provisioning or resolving the LibreChat user record before ACL evaluation and ensure the remote OIDC flow also refreshes or otherwise guarantees the LibreChat-side group membership state used for agent access decisions. would be cool to have it... maybe guarded by another config flag... |
We're facing the same issue. This can be implemented the same way openidStrategy does (reuse the processOpenIDAuth function). We plan to implement that in our fork for our needs. It would be great to have it by default, but I'm not sure where's the right place for it in LibreChat architecture. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eea9e9aff1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@danny-avila Hi, I've applied codex suggestions |
|
@danny-avila - I was today investigating the new admin ui and the new possibility to define roles for users... as a consequence I was creating a new enhancement request (#12769) - it would be great (required ;) ) that this then works here as well :) |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4beef5a937
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e5846f5fc1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@danny-avila - still works doing a quick check... any thoughts for #12450 (comment) and #12450 (comment) ? Would love to see both things as well - thank you! |
|
@codex review |
I think those are important and will definitely add to roadmap |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2598f0ec94
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bfb935073b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Thanks for the thorough work on this, everything looks great! No conflicts with our setup, working well. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a8547955c7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
1 similar comment
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 340d0a36f6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
|
Codex Review: Didn't find any major issues. Bravo. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
@codex review |
|
Codex Review: Didn't find any major issues. Nice work! ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
`IUser` extends mongoose `Document`, which types `id?: any` (the optional virtual). At runtime `id` is always `_id.toString()` for a hydrated doc, so narrow the type to a required string. Closes two `@rollup/plugin-typescript` TS2322 warnings introduced by PR #12450 (OIDC Bearer Token Authentication for Remote Agent API) where `req.user = userResolution.user` and the `(req: Request, res: Response, next: NextFunction)` signature both failed against the project's local `Express.User` augmentation (`{ [key: string]: any; id: string; }`) because `IUser.id` was `any`/optional. Narrowing here fixes both at the source rather than casting at every assignment site.
* 🐛 fix: Propagate User Identity to Subagent MCP Tool Calls
The `@librechat/agents` SDK's `SubagentExecutor` invokes the child
workflow with a fresh configurable of `{ thread_id }` only — the
parent's `user` / `user_id` are dropped on the way into the child
graph. The child's `ToolNode` then dispatches `ON_TOOL_EXECUTE` to the
parent's handler, which merges `{ ...configurable, ...toolConfigurable }`,
but neither side carries user identity for subagents.
Downstream MCP tools read `config.configurable.user?.id || user_id` and
got `undefined`, so `MCPManager.getConnection` fell through to the
"No connection found for server X" error path — it can't reach the
user-connection lookup without a userId.
Re-inject `user` (via `createSafeUser`) and `user_id` from `req.user`
into the configurable returned by `loadToolsForExecution`. This is the
single point all controllers (chat, Responses API, OpenAI-compat) flow
through. For the parent agent it's a no-op (outer config already
carries the same values); for subagents it fills the gap so MCP
connection lookup, user-placeholder substitution, and tools that read
configurable.user all work correctly.
* 🐛 fix: Preserve `api-user` Fallback When Injecting Subagent Identity
Codex review pointed out that the prior commit unconditionally wrote
`user_id: req.user?.id` (and `user`) into `toolConfigurable`. The handler
merges via `{ ...configurable, ...toolConfigurable }` — `toolConfigurable`
wins — so when `req.user` is absent, this overwrote the outer config's
`'api-user'` fallback (set by `responses.js` / `openai.js` for the
unauthenticated API-key path) with `undefined`, breaking MCP connection
lookup for that path.
Only inject the keys when `req.user.id` is truthy. Omitting them lets
the merge preserve whatever the outer configurable already had. Tests
updated to assert key omission for `req.user` undefined / null / present
without `id`.
* 🩹 fix: Narrow `IUser.id` to required string
`IUser` extends mongoose `Document`, which types `id?: any` (the optional
virtual). At runtime `id` is always `_id.toString()` for a hydrated doc,
so narrow the type to a required string.
Closes two `@rollup/plugin-typescript` TS2322 warnings introduced by
PR #12450 (OIDC Bearer Token Authentication for Remote Agent API)
where `req.user = userResolution.user` and the
`(req: Request, res: Response, next: NextFunction)` signature both
failed against the project's local `Express.User` augmentation
(`{ [key: string]: any; id: string; }`) because `IUser.id` was
`any`/optional. Narrowing here fixes both at the source rather than
casting at every assignment site.
* 🩹 fix: Resolve TS Build Warnings Surfaced by `IUser.id` Narrowing
Three rollup TS plugin warnings surfaced after narrowing
`IUser.id` from `any` to `string`:
- `utils/env.ts:95` — `safeUser[field] = user[field]` failed strict
checking because indexed write through a union-typed key collapses
the LHS to the intersection of all field write types (i.e.,
`undefined` when fields have mixed types). The previous `id?: any`
on IUser had been masking this. Switch to `Object.assign(safeUser,
{ [field]: user[field] })` which widens the assignment.
- `endpoints/google/initialize.ts:35` — `getUserKey({ userId:
req.user?.id, ... })` failed because `req.user?.id` is now
`string | undefined` (no longer `any`). Match the pattern already
used in `endpoints/openAI/initialize.ts:49`: `req.user?.id ?? ''`.
- `middleware/remoteAgentAuth.ts:465` — pre-existing, unrelated to
the IUser change. The local (gitignored) `express.d.ts` augments
`express.Request` but not `express-serve-static-core.Request`,
so the explicit `(req: Request, ...)` annotation imported from
`'express'` resolves to a Request whose `req.user` differs from
the one `RequestHandler` expects internally. Type the closure as
`RequestHandler` directly so TS infers params from the augmented
type.
* 🩹 fix: Cast `RemoteAgentAuth` Closure to `RequestHandler`
My previous attempt removed the explicit `req: Request` annotation on
the closure to side-step the outer `RequestHandler` mismatch. That
shifted the error to every helper call site inside the closure
(`getConfigOptions(req)`, `runApiKeyAuth(req, ...)` at 467/474/493/
512/531), because the helpers annotate their params with
`express.Request` (which has the local `Request.user` augmentation),
while the unannotated closure inferred `req` as
`express-serve-static-core.Request` (no augmentation). Reproduced
locally by stubbing the gitignored `src/types/express.d.ts`.
Right approach: keep the explicit `req: Request` annotation so the
closure body matches the helpers' types, then cast at the return —
`RequestHandler`'s internal `Request` resolves through
`express-serve-static-core` and lacks the augmentation, so the cast
is the boundary that bridges the two views of `req.user`.
Verified against a build with the local express.d.ts stub: zero
warnings on `remoteAgentAuth.ts`, `env.ts`, and `google/initialize.ts`.
…avila#12450) * Remote Agent Auth middleware * consider migration and update user * fix eslint errors * add scope validation * fix codex review errors * add filter for use: sig * add jwks-rsa deps * Fix remote agent OIDC auth review findings * Polish remote agent OIDC timeout coverage * Reject remote OIDC tokens without subject * Use tenant context for remote agent auth config * Harden remote agent OIDC scope handling * Polish remote agent OIDC cache and scope tests * Resolve remote agent auth review comments * Reuse OpenID email claim resolver for remote auth * Skip empty OpenID email fallback claims * Use pre-auth tenant context for remote auth config * Downgrade expected OIDC fallback logging * Require secure remote OIDC endpoints * Polish remote agent auth edge cases * Enforce unique balance records * Bind remote OpenID users to issuer * Fix issuer-scoped OpenID indexes * Avoid unique balance index requirement * Fix remote OpenID issuer normalization boundaries * Require issuer-bound OpenID lookups * Enforce tenant API key policy after auth * Fix remote auth tenant policy types * Normalize remote OIDC discovery issuer * Allow normalized remote OIDC issuer validation * Enforce resolved tenant OIDC policy * Polish OpenID issuer and scope validation --------- Co-authored-by: Danny Avila <danny@librechat.ai>
…a#12950) * 🐛 fix: Propagate User Identity to Subagent MCP Tool Calls The `@librechat/agents` SDK's `SubagentExecutor` invokes the child workflow with a fresh configurable of `{ thread_id }` only — the parent's `user` / `user_id` are dropped on the way into the child graph. The child's `ToolNode` then dispatches `ON_TOOL_EXECUTE` to the parent's handler, which merges `{ ...configurable, ...toolConfigurable }`, but neither side carries user identity for subagents. Downstream MCP tools read `config.configurable.user?.id || user_id` and got `undefined`, so `MCPManager.getConnection` fell through to the "No connection found for server X" error path — it can't reach the user-connection lookup without a userId. Re-inject `user` (via `createSafeUser`) and `user_id` from `req.user` into the configurable returned by `loadToolsForExecution`. This is the single point all controllers (chat, Responses API, OpenAI-compat) flow through. For the parent agent it's a no-op (outer config already carries the same values); for subagents it fills the gap so MCP connection lookup, user-placeholder substitution, and tools that read configurable.user all work correctly. * 🐛 fix: Preserve `api-user` Fallback When Injecting Subagent Identity Codex review pointed out that the prior commit unconditionally wrote `user_id: req.user?.id` (and `user`) into `toolConfigurable`. The handler merges via `{ ...configurable, ...toolConfigurable }` — `toolConfigurable` wins — so when `req.user` is absent, this overwrote the outer config's `'api-user'` fallback (set by `responses.js` / `openai.js` for the unauthenticated API-key path) with `undefined`, breaking MCP connection lookup for that path. Only inject the keys when `req.user.id` is truthy. Omitting them lets the merge preserve whatever the outer configurable already had. Tests updated to assert key omission for `req.user` undefined / null / present without `id`. * 🩹 fix: Narrow `IUser.id` to required string `IUser` extends mongoose `Document`, which types `id?: any` (the optional virtual). At runtime `id` is always `_id.toString()` for a hydrated doc, so narrow the type to a required string. Closes two `@rollup/plugin-typescript` TS2322 warnings introduced by PR danny-avila#12450 (OIDC Bearer Token Authentication for Remote Agent API) where `req.user = userResolution.user` and the `(req: Request, res: Response, next: NextFunction)` signature both failed against the project's local `Express.User` augmentation (`{ [key: string]: any; id: string; }`) because `IUser.id` was `any`/optional. Narrowing here fixes both at the source rather than casting at every assignment site. * 🩹 fix: Resolve TS Build Warnings Surfaced by `IUser.id` Narrowing Three rollup TS plugin warnings surfaced after narrowing `IUser.id` from `any` to `string`: - `utils/env.ts:95` — `safeUser[field] = user[field]` failed strict checking because indexed write through a union-typed key collapses the LHS to the intersection of all field write types (i.e., `undefined` when fields have mixed types). The previous `id?: any` on IUser had been masking this. Switch to `Object.assign(safeUser, { [field]: user[field] })` which widens the assignment. - `endpoints/google/initialize.ts:35` — `getUserKey({ userId: req.user?.id, ... })` failed because `req.user?.id` is now `string | undefined` (no longer `any`). Match the pattern already used in `endpoints/openAI/initialize.ts:49`: `req.user?.id ?? ''`. - `middleware/remoteAgentAuth.ts:465` — pre-existing, unrelated to the IUser change. The local (gitignored) `express.d.ts` augments `express.Request` but not `express-serve-static-core.Request`, so the explicit `(req: Request, ...)` annotation imported from `'express'` resolves to a Request whose `req.user` differs from the one `RequestHandler` expects internally. Type the closure as `RequestHandler` directly so TS infers params from the augmented type. * 🩹 fix: Cast `RemoteAgentAuth` Closure to `RequestHandler` My previous attempt removed the explicit `req: Request` annotation on the closure to side-step the outer `RequestHandler` mismatch. That shifted the error to every helper call site inside the closure (`getConfigOptions(req)`, `runApiKeyAuth(req, ...)` at 467/474/493/ 512/531), because the helpers annotate their params with `express.Request` (which has the local `Request.user` augmentation), while the unannotated closure inferred `req` as `express-serve-static-core.Request` (no augmentation). Reproduced locally by stubbing the gitignored `src/types/express.d.ts`. Right approach: keep the explicit `req: Request` annotation so the closure body matches the helpers' types, then cast at the return — `RequestHandler`'s internal `Request` resolves through `express-serve-static-core` and lacks the augmentation, so the cast is the boundary that bridges the two views of `req.user`. Verified against a build with the local express.d.ts stub: zero warnings on `remoteAgentAuth.ts`, `env.ts`, and `google/initialize.ts`.
Summary
Adds OIDC Bearer token authentication support for the Remote Agent API (
endpoints.agents.remoteApi). Previously only API key auth was supported. The new middleware validates Bearer tokens against a configured OIDC issuer via JWKS, with a fallback to API key auth when enabled.Closes #11733, closes #11640
How it works
endpoints.agents.remoteApi.authfromAppConfigoidc.enabled: trueattempts to verify the Bearer token via JWKSjwksUriinlibrechat.yamlOPENID_JWKS_URLenv var{issuer}/.well-known/openid-configurationfindOpenIDUserand attaches it toreq.userapiKeyMiddlewareifapiKey.enabled !== false, otherwise returns401Config example
Change Type
Testing
Tested interaction flow through
/agents/v1endpoint with OIDC Keycloak instance.Also unit tests are written.
Checklist