Repository navigation
Conversation
The session JWT is read from Authorization, which is unavailable when a proxy in front of LibreChat writes that header itself (oauth2-proxy with --set-authorization-header, or a mesh or gateway doing credential injection). JWT_AUTH_HEADER names a header to try first, unset by default, with Authorization kept as the fallback. The value must still be a JWT signed with JWT_SECRET, so no new trust is introduced. Discussed in LibreChat-AI#14752.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1617e197b4
ℹ️ 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".
| const jwtExtractor = () => { | ||
| const extractors = []; | ||
| if (process.env.JWT_AUTH_HEADER) { | ||
| extractors.push(ExtractJwt.fromHeader(process.env.JWT_AUTH_HEADER)); |
There was a problem hiding this comment.
Normalize the configured header name before extraction
When an operator configures a conventionally capitalized name such as X-Original-Authorization, Node exposes the incoming key in req.headers as lowercase, while passport-jwt's fromHeader looks up the supplied name directly. Consequently, the custom token is never found and authentication still falls back to the proxy-overwritten Authorization header; lowercase JWT_AUTH_HEADER before constructing the extractor and cover mixed-case configuration in the test.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch. fromHeader looks the key up as-is and node lowercases incoming headers, so a capitalised JWT_AUTH_HEADER would have silently fallen through to Authorization — the exact failure the option exists to prevent. Now trimmed and lowercased, with tests for mixed case and for a whitespace-only value.
Node lowercases incoming header names and fromHeader looks the given key up as-is, so a conventionally capitalised value never matched and authentication fell back to the Authorization header the proxy had overwritten — silently. The value is now trimmed and lowercased, and a whitespace-only value is treated as unset. Covered by tests for mixed case and for whitespace.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4bf960f267
ℹ️ 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".
| const jwtExtractor = () => { | ||
| const extractors = []; | ||
| // node lowercases incoming header names; fromHeader looks the key up as-is | ||
| const header = process.env.JWT_AUTH_HEADER?.trim().toLowerCase(); | ||
| if (header) { |
There was a problem hiding this comment.
Move configurable extraction into packages/api
This introduces the complete configurable JWT-extractor implementation directly in the legacy /api JavaScript workspace. Repository policy requires new backend logic to be TypeScript under /packages/api and limits /api to thin wrappers, so the extractor builder should live in the TypeScript package and be called from this strategy. CLAUDE.mdL20-L23
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
| # Optional: read the session JWT from this header before falling back to Authorization. | ||
| # Only needed behind a proxy that overwrites Authorization with its own token | ||
| # (e.g. oauth2-proxy with --set-authorization-header). Leave unset otherwise. | ||
| # JWT_AUTH_HEADER=x-original-authorization |
There was a problem hiding this comment.
Document the required raw-token header format
When a proxy copies LibreChat's original Authorization value verbatim into the suggested x-original-authorization header, the value is Bearer <jwt> because LibreChat emits session credentials in that form (for example, packages/data-provider/src/headers-helpers.ts:11). ExtractJwt.fromHeader does not remove the scheme, so authentication rejects the copied value, but this operator-facing documentation never says that the proxy must strip Bearer . State that requirement here or make the alternate extractor accept both forms.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Resolved by accepting both forms: the extractor strips an optional Bearer scheme (case-insensitive) before returning the token, so a proxy can copy Authorization verbatim. .env.example and librechat.example.yaml document it, and jwt.spec.ts covers raw, Bearer and bearer values (898cdeb).
Move the configurable-header extraction into a typed createJwtExtractor helper in packages/api, keeping jwtStrategy.js as a thin wrapper. The configured header now also accepts a value that still carries the Bearer scheme, so a proxy copying Authorization verbatim works, and a header sent more than once is ignored in favour of the Authorization fallback rather than guessed.
# Conflicts: # api/strategies/jwtStrategy.js # api/strategies/jwtStrategy.spec.js # packages/api/src/auth/index.ts
Add `sessionToken.header` to the config schema, carried through AppConfig and resolved ahead of `JWT_AUTH_HEADER`, so the header follows the yaml-first, env-fallback pattern of other auth settings. The base config is passed to the JWT strategy at startup; with neither source set only `Authorization` is read, as before. Invalid header names are rejected by the schema and ignored, with a warning, from the env. Also fix duplicated headers: Node joins a repeated header into one comma-separated string rather than an array, so the extractor read a mangled token and returned 401 instead of falling back. A JWT never contains a comma, so such a value is now ignored. The test sends the duplicate through a real HTTP server instead of a hand-built array.
|
@codex review Head: |
|
To use Codex here, create a Codex account and connect to github. |
|
Hi @danny-avila, a quick note on this one: since the last Codex pass I merged |
|
To use Codex here, create a Codex account and connect to github. |
Pull Request
Summary
Related to #14752
api/strategies/jwtStrategy.jsreads the session JWT only fromAuthorization. When something in front of LibreChat also writes that header — oauth2-proxy with--set-authorization-header=true, or a mesh or API gateway doing upstream credential injection — the strategy receives the proxy's token instead of the one LibreChat issued and returns401 {"message":"invalid algorithm"}, even on requests where the browser sent noAuthorizationat all. The substitution happens before the request reaches Node, so there is nothing to configure around today.This adds an optional header that is read first, with
Authorizationkept as the fallback. It is set assessionToken.headerinlibrechat.yaml, falling back to theJWT_AUTH_HEADERenv var and then to today's behaviour when neither is given, the same yaml-first pattern aspasskeys.perUserMax. The proxy can copy the originalAuthorizationvalue verbatim: bothBearer <token>and a bare token are accepted, and the name is case-insensitive. A header that arrives more than once is ignored rather than guessed, soAuthorizationdecides.Scope: only
jwtStrategychanges.openIdJwtStrategyhas the same extraction line but verifies an IdP token through JWKS and an audience check, and the standard header is exactly where a proxy like this puts an IdP token, so it is left alone deliberately.Security: no new trust is introduced. Whatever is read from the header must still be a JWT signed with
JWT_SECRET, so setting the header without the secret gets nothing. This is not #1856 / #8562, which ask LibreChat to trust an identity asserted by the proxy; here the token is still LibreChat's own and only its location is configurable.How it works
The
/apistrategy stays wiring; resolution and extraction live inpackages/api/src/auth/jwt.ts:With no header configured
createJwtExtractorreturns the originalAuthorizationextractor unchanged. Node joins a repeated header into one comma-separated string, and a JWT never contains a comma, so a comma marks a duplicated header and falls through toAuthorization. The header is resolved once when the strategy is built, so a change takes effect on restart; the yaml comment and.env.examplesay so.Type of change
Testing
Tested environments/configuration: macOS, Node 26; the process-group hook suites, which read
/proc, were also run in anode:24.16.0Linux container.Automated tests:
packages/api/src/auth/jwt.spec.ts: extraction (raw andBearervalues, case-insensitive name, absent, empty or scheme-only values fall back), a duplicated header sent through a real HTTP server, andresolveJwtAuthHeaderprecedence (yaml over env, env when yaml is unset, none when neither is set, invalid names ignored).api/strategies/jwtStrategy.spec.js: the strategy builds the extractor from yaml ahead ofJWT_AUTH_HEADER, from the env when yaml is unset, and with no header otherwise.packages/data-provider/src/config.spec.ts:sessionToken.headeris optional, trimmed, and rejects names a request could not carry.packages/api/src/app/AppService.spec.ts: the yaml value reaches the effective config.api,packages/api,packages/data-schemasandpackages/data-providersuites were run on the merge withdev; the only failures were timing- or macOS-specific suites outside this change, which pass on their own or on Linux. After the final commit,packages/api/src/auth,AppService,api/strategiesand the data-provider config suite were rerun and pass.npx tsc --noEmitin each changed workspace andnpm run static-checks:full -- --against origin/devpass.Screenshots / recordings
No user-facing change.
Risk / compatibility
Additive and off by default: with neither
sessionToken.headernorJWT_AUTH_HEADERset, the strategy uses the sameAuthorizationextractor as before. The new config field is optional, so existinglibrechat.yamlfiles validate unchanged.