Skip to content

🔑 feat: allow the session JWT to be read from a configurable header - #14753

Open
apiloqbc wants to merge 5 commits into
LibreChat-AI:devfrom
apiloqbc:fix/jwt-auth-header
Open

apiloqbc wants to merge 5 commits into
LibreChat-AI:devfrom
apiloqbc:fix/jwt-auth-header

Conversation

@apiloqbc

@apiloqbc apiloqbc commented Aug 11, 2026 •

Copy link
Copy Markdown

Pull Request

Summary

Related to #14752

api/strategies/jwtStrategy.js reads the session JWT only from Authorization. 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 returns 401 {"message":"invalid algorithm"}, even on requests where the browser sent no Authorization at 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 Authorization kept as the fallback. It is set as sessionToken.header in librechat.yaml, falling back to the JWT_AUTH_HEADER env var and then to today's behaviour when neither is given, the same yaml-first pattern as passkeys.perUserMax. The proxy can copy the original Authorization value verbatim: both Bearer <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, so Authorization decides.

Scope: only jwtStrategy changes. openIdJwtStrategy has 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 /api strategy stays wiring; resolution and extraction live in packages/api/src/auth/jwt.ts:

startServer (api/server/index.js)
  getAppConfig({ baseOnly: true })          # base config: the strategy is process-wide
  passport.use(jwtLogin(appConfig))
    resolveJwtAuthHeader(appConfig.sessionToken)   # yaml > JWT_AUTH_HEADER > none
    createJwtExtractor(header, fromAuthHeaderAsBearerToken())
      req.headers[header]                   # lowercased name, optional "Bearer ", no comma
      ?? Authorization                      # fallback, unchanged

With no header configured createJwtExtractor returns the original Authorization extractor 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 to Authorization. The header is resolved once when the strategy is built, so a change takes effect on restart; the yaml comment and .env.example say so.

Type of change

  • Feature
  • Documentation

Testing

Tested environments/configuration: macOS, Node 26; the process-group hook suites, which read /proc, were also run in a node:24.16.0 Linux container.

Automated tests:

  • packages/api/src/auth/jwt.spec.ts: extraction (raw and Bearer values, case-insensitive name, absent, empty or scheme-only values fall back), a duplicated header sent through a real HTTP server, and resolveJwtAuthHeader precedence (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 of JWT_AUTH_HEADER, from the env when yaml is unset, and with no header otherwise.
  • packages/data-provider/src/config.spec.ts: sessionToken.header is optional, trimmed, and rejects names a request could not carry.
  • packages/api/src/app/AppService.spec.ts: the yaml value reaches the effective config.
  • The full api, packages/api, packages/data-schemas and packages/data-provider suites were run on the merge with dev; 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/strategies and the data-provider config suite were rerun and pass.
  • npx tsc --noEmit in each changed workspace and npm run static-checks:full -- --against origin/dev pass.

Screenshots / recordings

No user-facing change.

Risk / compatibility

Additive and off by default: with neither sessionToken.header nor JWT_AUTH_HEADER set, the strategy uses the same Authorization extractor as before. The new config field is optional, so existing librechat.yaml files validate unchanged.

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.
@danny-avila

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread api/strategies/jwtStrategy.js Outdated
const jwtExtractor = () => {
const extractors = [];
if (process.env.JWT_AUTH_HEADER) {
extractors.push(ExtractJwt.fromHeader(process.env.JWT_AUTH_HEADER));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@danny-avila

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread api/strategies/jwtStrategy.js Outdated
Comment on lines +12 to +16
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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 9adbe5c: the extractor now lives in packages/api/src/auth/jwt.ts (createJwtExtractor, plus resolveJwtAuthHeader since 898cdeb), and api/strategies/jwtStrategy.js only requires it and passes the base config in. Covered by packages/api/src/auth/jwt.spec.ts.

Comment thread .env.example
Comment on lines +696 to +699
# 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

@danny-avila danny-avila added 🗺️ Auth OAuth Infra codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) 🛡️ security review labels Sep 24, 2026
@codegraph-librechat codegraph-librechat Bot added 🗺️ Security & Access codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) 🗺️ Server Core codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) 🗺️ Backend Infra codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) labels Oct 1, 2026
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.
@apiloqbc
apiloqbc requested a review from danny-avila as a code owner October 8, 2026 11:39
@apiloqbc
apiloqbc changed the base branch from main to dev October 8, 2026 11:39
@apiloqbc

apiloqbc commented Oct 8, 2026

Copy link
Copy Markdown
Author

@codex review

Head: 898cdeba547a304064c6c11769700b7f03704126 (merge of dev, then sessionToken.header in librechat.yaml and the duplicated-header fix).

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@codegraph-librechat codegraph-librechat Bot added 🗺️ Identity & Billing codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) 🗺️ Auth Strategies codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) labels Oct 9, 2026
@apiloqbc

apiloqbc commented Oct 13, 2026 •

Copy link
Copy Markdown
Author

Hi @danny-avila, a quick note on this one: since the last Codex pass I merged dev, made the header configurable via sessionToken.header in librechat.yaml, and fixed the duplicated-header case (head 898cdeba54). I can't trigger Codex myself since I'm not a maintainer. Could you kick off a review when you get a chance? Happy to address any feedback. Thanks!

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@codegraph-librechat codegraph-librechat Bot added the 🗺️ Auth & Sessions codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) label Oct 15, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🗺️ Auth OAuth Infra codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) 🗺️ Auth & Sessions codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) 🗺️ Auth Strategies codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) 🗺️ Backend Infra codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) 🗺️ Identity & Billing codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) 🗺️ Security & Access codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) 🛡️ security review 🗺️ Server Core codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants