Two of the five findings from the Codex pass on fcdc15885 — the two that are
defects in code this branch introduced rather than design questions about the
bridge.
`getSkewedTokenExpiresAtMs` floored every result at a second in the future,
including an expiry the provider had already declared elapsed. An exchange
answering `expires_in: 0` or a past `expires_at` was handed to the MCP
connection stamped valid for another second, which only moves the failure
downstream. The floor now applies to a lifetime that is still live, which is
what it was for; an elapsed one stays elapsed so the caller rejects it. Same
for the cache TTL, which falls back to the elapsed-credential floor.
Bridge recovery left the stale token's durable Session behind. Only the token
it recovered through was passed as `existingRefreshToken`, so that one's
session was replaced while the token the browser actually presented kept its
record until its original expiry. That record, with the marker cookie still
bound to it, is what authorizes local image access for OpenID users — so a
copy of the stale cookie outlived the rotation it had lost. Revoked
explicitly on successful recovery.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F
Merge jcbartle's #13901 up to date with main (307 commits since its last
merge at 5ff282f90) and reconcile it with what landed in the meantime.
OBO exchange read `user.federatedTokens`, the snapshot captured at strategy
time, so a user who idled past their access-token lifetime got "No valid
OpenID access token is available for OBO exchange" while a still-refreshable
token sat unread in `req.session.openidTokens` (#13899). Exchange now reads
the live server-side session at MCP call time and refreshes the upstream
token inline, single-flighted across workers through a Mongo refresh-flight
record, with an encrypted bridge so a token rotated mid-SSE stream (headers
already sent, cookie unwritable) stays recoverable on the next /refresh.
Reconciled against main:
- Exchanged-token cache TTLs now carry the in-transit expiry buffer (#15320).
The PR's `mcp/oauth/expiry.ts` and main's `oauth/expiry.ts` (#15321) were
two homes for the same arithmetic; they are now one. `getTokenCacheTtlMs`
subtracts `OPENID_EXPIRY_BUFFER_SECONDS`, which also covers the Graph and
openidStrategy caches, and the skew helpers build on main's
`normalizeExpiresIn` so an elapsed lifetime still stays elapsed.
`OPENID_EXPIRY_BUFFER_SECONDS` moves to `oauth/expiry.ts`, where the rest
of the lifetime math lives: consumers that stub `~/utils/oidc` were
leaving the arithmetic reading `undefined`.
- The `openid_user_id` marker keeps the refresh-token binding main added in
#15252, now inside the shared `setOpenIDMarkerCookies` helper so the
inline-refresh path produces the same bound cookie as login.
- Every OpenID token issuance in `refreshController` — including bridge
recovery, which previously issued tokens without one — persists the
durable Session record through `sendOpenIDAuthResponse`.
- An inline refresh that rotates the token and writes the cookie moves the
durable Session record with it. Local image access for OpenID users is
authorized by that record keyed on the browser's cookie, so leaving it
behind 401s every image until the next /refresh. The bridge path
deliberately leaves it alone: there the browser keeps the old cookie.
Co-authored-by: jcbartle <7274202+jcbartle@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F
* 🪪 fix: Preserve Stored Access Token Expiry Over ID Token Exp
extractOpenIDTokenInfo let the ID token exp claim overwrite the token set's stored expires_at. The ID token is minted at login and never refreshed, so once a session outlives the ID token TTL, isOpenIDTokenValid reports the access token as expired even when expires_at is hours in the future, and OpenID placeholder substitution silently stops: MCP headers configured with {{LIBRECHAT_OPENID_ACCESS_TOKEN}} ship the literal placeholder string as the bearer credential and the receiving server rejects every connection with an unparseable JWT until the user fully logs out and back in.
The ID token exp now only fills a missing expiresAt instead of overriding a stored one. Identity claim enrichment from the ID token is unchanged, and the exp fallback for token sets without expires_at is preserved.
* 🪪 fix: Validate ID Token Expiry Before ID Token Placeholder Substitution
The precedence fix made isOpenIDTokenValid track only the access token expiry, so an MCP header using {{LIBRECHAT_OPENID_ID_TOKEN}} could substitute an ID token that had already expired. The ID token exp is now preserved separately as idTokenExpiresAt and checked at the ID token substitution site, so an expired ID token substitutes empty rather than a stale credential while access token substitution is unaffected.
* 🪪 fix: Address OpenID Expiry Review Round
Fix expires_at at the source in the OpenID JWT strategy. The stored value described the
incoming bearer's exp even when access_token came from the session or a cookie, so it could
describe a different credential entirely. A new decodeJwtExpiry helper reads the exp of the
token actually stored, and payload.exp is kept only when the raw bearer is the resolved
access token. Opaque session or cookie tokens now store no expiry rather than a wrong one.
Apply a 30 second clock skew buffer in isOpenIDTokenValid and isIdTokenCurrent via a new
exported OPENID_EXPIRY_BUFFER_SECONDS, mirroring OPENID_REUSE_EXPIRY_BUFFER_SECONDS in
AuthController. Tokens that would expire in transit are treated as already expired.
Make isIdTokenCurrent fail closed when idTokenExpiresAt is absent. exp is REQUIRED in an ID
token, so a missing value means the token is malformed or the claims parse threw. The check
uses == null so an exp of 0 counts as present and therefore expired.
Read the ID token exp with a numeric type check so an exp of 0 records idTokenExpiresAt and
fails closed downstream while a non-numeric exp is ignored, and compare the stored expiry
with != null so a gap filled expiry of 0 reads as expired instead of as no expiry at all.
Raise an actionable re authentication error for the ID token placeholder instead of
substituting an empty string. An empty substitution produced a malformed Authorization
header and a 400 downstream rather than a clean signal that the user must re authenticate.
Raise the same re authentication error from processSingleValue when a user has an OpenID
identity, the stored token set is no longer valid, and the value still contains a credential
bearing OpenID placeholder, so the expired access token case that motivated this PR signals
re auth instead of silently shipping or stripping the placeholder. Only the access token, ID
token, and generic token names raise: identity metadata resolves from the user document and
an expiry hint never needed a token, so those keep their existing literal then strip
behaviour. Unknown placeholder names also stay literal and diagnosable, matching the
existing resolvable placeholder policy.
Add the comments the review asked for on the exp fallback heuristic, the EXPIRES_AT
placeholder semantics, why stale ID token claims stay usable for identity fields, and the
advisory nature of the freshness check.
* 🪪 fix: Honour Opaque Access Tokens And Type The OpenID Re-Auth Error
Drop the ID token exp fallback in extractOpenIDTokenInfo. Storing the access token expiry
honestly means an opaque access token now records no expiry, and the fallback then handed the
ID token exp authority over a credential it does not describe. A deployment issuing opaque
access tokens alongside a short lived ID token saw isOpenIDTokenValid go false and the
credential guard reject a perfectly good access token, which worked before this branch. An
unknown access token expiry is now treated as no expiry, and the ID token exp only ever gates
ID token substitution through idTokenExpiresAt.
Give the re-authentication signal a type. OpenIDReauthRequiredError is raised at both the ID
token placeholder and the credential placeholder guard, ErrorController maps it to a 401
carrying the actionable message, and the class exposes statusCode so the agent generation
path answers 401 instead of a bare 500 for the same condition.
Omit rather than blank a header whose credential placeholder is still unresolved on a final
resolution pass, since an empty bearer credential is malformed under RFC 6750 while an absent
header lets the upstream answer its own challenge. Identity placeholders keep stripping to an
empty string.
Move the resolvable placeholder docblock onto the pattern it describes, resolve an EXPIRES_AT
of 0 as the string 0 for consistency with the neighbouring null checks, and let
AuthController consume the exported OPENID_EXPIRY_BUFFER_SECONDS so the 30 second skew
allowance has a single definition.
Move resolveToolCallUserId inside the tool-call try/catch so an OBO
identity mismatch surfaces with serverName/toolName context and the
standard tool-call-failed message instead of an opaque bare Error.
Raise the refresh-token bridge lookup failure log from debug to warn so
transient infrastructure failures on the unauthenticated /api/auth/refresh
path are observable, and guard the message access against non-Error values.
Replace the unknown+cast in isDuplicateKeyError with a hasErrorCode type
predicate so the duplicate-key check reads error.code without an assertion.
Preserve real math/isEnabled in the MCPConnectionFactory test mock (mock
only processMCPEnv) so mcpConfig timeouts no longer resolve to NaN, fixing
the TimeoutNaNWarning that masked slow OAuth retry behavior.
Reuse getValidOpenIDReuseUserId for the bridge-recovery user lookup in
refreshController instead of re-verifying openid_user_id inline. The shared
helper enforces the JWT_REFRESH_SECRET presence check and a strict
typeof payload.id === 'string' guard, rejecting tokens whose id claim is
present but not a string (e.g. a numeric id) that the inline check accepted.
Fail closed on issuer mismatch in getRefreshTokenBridge. Both the stored and
the expected issuer are now normalized and compared for equality, so a bridge
is recovered only when both sides agree (both absent, or both present and
equal after normalization). Previously the check was skipped whenever the
stored issuer was absent, allowing recovery across mismatched issuer context.
Drop the unused {oldRefreshTokenHash, userId, tenantId, openidIssuer} index
and the openidIssuer field on RefreshTokenBridgeQuery. The data-layer filter
only queries the 3-field {oldRefreshTokenHash, userId, tenantId} index; the
issuer is verified in application code, not the query. Hoist the repeated
model accessor into getRefreshTokenBridgeModel.
Note: issuer is now load-bearing for recovery. A bridge stored with an issuer
recovers only when the lookup supplies a matching issuer; the recovery lookup
reads user.openidIssuer via AUTH_REFRESH_USER_PROJECTION (an exclusion
projection that retains the field). If a user's persisted openidIssuer is
empty while the stored bridge has one, recovery fails closed (falls through to
normal re-authentication) until the bridge TTLs out — no security regression.
Tests cover invalid signed-cookie payloads bypassing the bridge, both
asymmetric issuer-presence cases, issuer normalization before comparison, and
an index-alignment assertion guarding against re-adding the dropped index.
Prefer the browser refresh-token cookie when it differs from the
server-side OpenID session state, and force a real IdP refresh in that
case instead of reusing stale session tokens.
Store a short-lived refresh-token bridge when inline OBO refresh writes
a rotated browser cookie but session persistence fails, so follow-up
refreshes can still recover from the old token.
Keep the bridge grace TTL centralized in RefreshTokenBridge so both
recovery paths use the same env-backed value.
Note: drift is measured against the last-synced browserRefreshToken
marker, so the SSE path (intentionally stale cookie, authoritative
session) does not false-positive. Sessions predating the marker have no
browserRefreshToken; for those, drift falls back to comparing the cookie
against the session refresh token and prefers the cookie on difference.
This is the same self-healing pre-change-session window as the identity
binding fix and re-syncs within one session lifetime.
Tests cover cookie/session drift selection, reusable-session bypass on
drift, bridge storage after session-save failure, and the shared bridge
constant wiring.
Stamp OpenID session token state with the LibreChat user id, OpenID subject,
tenant id, and normalized issuer when tokens are stored.
Fail closed before OBO inline token reuse/refresh when the session token
identity does not match the current authenticated identity, preventing a stale
or mixed Express session from supplying another user's upstream assertion.
Also validate the normal /api/auth/refresh session-token reuse shortcut against
the signed marker-cookie user before returning cached session tokens.
Note: sessions created before this change carry no identity stamp and are
treated as a mismatch. This is self-healing — the reuse path forces a full IdP
refresh (which re-stamps the session) and the OBO path throws, surfacing as a
one-time re-authentication for active OBO users at deploy time. The session
re-stamps within one session lifetime (SESSION_EXPIRY, default 15 min).
Extract the shared OpenID refresh/user-resolution flow in AuthController
so the normal refresh path and bridge-recovery retry use the same grant,
claims, issuer, user lookup, and diagnostic logging code.
Preserve the existing path-specific behavior: the normal path still owns
migration updates and 401 login redirects, while the bridge retry still
falls through to the existing 403 invalid-token response.
Add a bridge-recovery guard that rejects retry results whose resolved
user id differs from the signed openid_user_id cookie before issuing
tokens or re-storing the grace bridge. Cover both the successful
matching-user recovery and the mismatched-user rejection.
After successful bridged refresh recovery, re-store the stale-cookie bridge
with a short grace TTL instead of deleting it immediately. This lets parallel
/api/auth/refresh requests that already sent the stale browser cookie recover
before they can observe the first response's Set-Cookie.
Retarget the bridge to the refresh token returned by the bridged retry so
B-to-C refresh-token rotation remains recoverable. The grace TTL is parsed with
math() and defaults to 60s, which shrinks the replay window from the original
REFRESH_TOKEN_EXPIRY bridge lifetime to the short recovery grace period.
Remove the now-unused explicit bridge delete path from the service and
data-schemas method surface. Add regression coverage for grace re-store,
identity symmetry, retry failure behavior, and same-key upsert replacement.
Make refresh-token bridge cleanup best-effort after a bridged OIDC
refresh succeeds. A transient delete failure now logs a warning but does
not convert the already-refreshed session and cookies into a 403 response.
Store SSE OBO refresh-token recovery bridges in MongoDB instead of
process-local memory so /api/auth/refresh can recover after worker
restarts or cross-worker routing.
Derive bridge expiry from REFRESH_TOKEN_EXPIRY so the recovery window
matches the stale refreshToken cookie it repairs, and delete bridges
after successful recovery.
When an inline OBO refresh rotates the OpenID refresh token after SSE headers
have already been sent, the browser refreshToken cookie cannot be updated. Store
a short-lived encrypted bridge from the stale cookie token to the rotated token
so /api/auth/refresh can recover after express-session loss.
Use the signed openid_user_id cookie to load user context for bridge validation,
retry only on invalid_grant, and delete the bridge only after the bridged refresh
succeeds.
* feat: make OpenID token reuse window configurable via OPENID_REUSE_MAX_SESSION_AGE_MS
The OpenID session-token reuse window in AuthController was a hardcoded 15-minute
constant, forcing /api/auth/refresh to perform a real refreshTokenGrant against the
IdP every 15 minutes even when the current access token is still valid. IdPs that
rotate and revoke the previous access token on refresh then invalidate a token that
is still in use by downstream consumers of the reused OpenID token (e.g. MCP servers
that receive {{LIBRECHAT_OPENID_TOKEN}} and introspect the bearer), producing
~15-minute 401 cycles regardless of the access token's actual lifetime.
Read the window from process.env.OPENID_REUSE_MAX_SESSION_AGE_MS via the existing
math() helper, so it accepts an arithmetic expression like SESSION_EXPIRY (e.g.
60 * 60 * 24 * 1000), defaulting to the existing 15 minutes so behavior is unchanged
unless explicitly configured. The existing 30s-before-expiry guard still forces a
refresh before genuine expiry, so a larger window remains safe.
* fix: extend OpenID reuse session lifetime
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* 🔒 fix: Remove OpenID federated tokens from refresh endpoint response
The refresh controller was attaching federatedTokens (including the
refresh_token) to the user object returned in the JSON response,
exposing HttpOnly-protected tokens to client-side JavaScript.
The tokens are already stored server-side by setOpenIDAuthTokens
and re-attached by the JWT strategy on authenticated requests.
* 🔒 fix: Strip sensitive fields from OpenID refresh response user object
The OpenID refresh path returned the raw findOpenIDUser result without
field projection, unlike the non-OpenID path which excludes password,
__v, totpSecret, and backupCodes via getUserById projection. Destructure
out sensitive fields before serializing.
Also strengthens the regression test: uses not.toHaveProperty for true
property-absence checks (expect.anything() misses null/undefined), adds
positive shape assertion, and DRYs up duplicated mock user setup.
* Allow setting the claim field to be used when OpenID login is configured
* fix(openid): harden getOpenIdEmail and expand test coverage
Guard against non-string claim values in getOpenIdEmail to prevent a
TypeError crash in isEmailDomainAllowed when domain restrictions are
configured. Improve warning messages to name the fallback chain
explicitly and distinguish missing vs. non-string claim values.
Fix the domain-block error log to record the resolved identifier rather
than userinfo.email, which was misleading when OPENID_EMAIL_CLAIM
resolved to a different field (e.g. upn).
Fix a latent test defect in openIdJwtStrategy.spec.js where the
~/server/services/Config mock exported getCustomConfig instead of
getAppConfig, the symbol actually consumed by openidStrategy.js.
Add refreshController tests covering the OPENID_EMAIL_CLAIM paths,
which were previously untested despite being a stated fix target.
Expand JWT strategy tests with null-payload, empty/whitespace
OPENID_EMAIL_CLAIM, migration-via-preferred_username, and call-order
assertions for the findUser lookup sequence.
* test(auth): enhance AuthController and openIdJwtStrategy tests for openidId updates
Added a new test in AuthController to verify that the openidId is updated correctly when a migration is triggered during the refresh process. Expanded the openIdJwtStrategy tests to include assertions for the updateUser function, ensuring that the correct parameters are passed when a user is found with a legacy email. This improves test coverage for OpenID-related functionality.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
- Removed the extraction of access token from the Authorization header.
- Implemented logic to use the federated access token from the user object.
- Added error handling for missing federated access token.
- Updated related documentation in GraphTokenService to reflect changes in access token usage.
- Introduced unit tests for various scenarios in AuthController.spec.js to ensure proper functionality.
* feat: Implement reconnection staggering and backoff jitter for MCP connections
- Enhanced the reconnection logic in OAuthReconnectionManager to stagger reconnection attempts for multiple servers, reducing the risk of connection storms.
- Introduced a backoff delay with random jitter in MCPConnection to improve reconnection behavior during network issues.
- Updated the ConnectionsRepository to handle multiple server connections concurrently with a defined concurrency limit.
Added tests to ensure the new reconnection strategy works as intended.
* refactor: Update MCP server query configuration for improved data freshness
- Reduced stale time from 5 minutes to 30 seconds to ensure quicker updates on server initialization.
- Enabled refetching on window focus and mount to enhance data accuracy during user interactions.
* ♻️ refactor: On-demand MCP connections; remove proactive reconnection, default to available
- Remove reconnectServers() from refresh controller (connection storm root cause)
- Stop gating server selection on connection status; add to selection immediately
- Render agent panel tools from DB cache, not live connection status
- Proceed to cached tools on init failure (only gate on OAuth)
- Remove unused batchToggleServers()
- Reduce useMCPServersQuery staleTime from 5min to 30s, enable refetchOnMount/WindowFocus
* refactor: Optimize MCP tool initialization and server connection logic
- Adjusted tool initialization to only occur if no cached tools are available, improving efficiency.
- Updated comments for clarity on server connection and tool fetching processes.
- Removed unnecessary connection status checks during server selection to streamline the user experience.
* fix(auth): pass scope parameter in OpenID refresh token grant
When using Azure Entra ID with a custom API scope (e.g., api://app-id/access_user)
and OPENID_REUSE_TOKENS=true, the refresh token exchange fails with AADSTS90009
because the scope parameter is not included in the refresh request.
Azure AD v2.0 requires the scope parameter when refreshing tokens issued for
custom API audiences. Without it, Azure interprets the request as the app
requesting a token for itself and rejects it.
This fix passes OPENID_SCOPE as the scope parameter to refreshTokenGrant(),
maintaining backward compatibility (no scope sent if OPENID_SCOPE is not set).
Fixes: refresh token 400 error with Azure custom API scopes
Tested: Azure Entra ID + Token Reuse + SharePoint integration
* style(auth): fix ESLint multiline arguments formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: OpenID token handling by storing tokens in session to reduce cookie size
* refactor: Improve OpenID user identification logic in logout controller
* refactor: Enhance OpenID logout flow by adding post-logout redirect URI
* refactor: Update logout process to clear additional OpenID user ID cookie
The refreshController was not updating user openidId when found by email
with a different openidId already set. This caused an infinite loop where:
1. JWT auth failed (openidId mismatch)
2. Refresh found user by email but didn't update openidId
3. Next JWT auth failed again → loop
Now updates openidId when either migration is needed OR when the stored
openidId doesn't match the token's sub claim. Added debug logging for
visibility into auth state during refresh.
* feat: Add OpenID Connect federated provider token support
Implements support for passing federated provider tokens (Cognito, Azure AD, Auth0)
as variables in LibreChat's librechat.yaml configuration for both custom endpoints
and MCP servers.
Features:
- New LIBRECHAT_OPENID_* template variables for federated provider tokens
- JWT claims parsing from ID tokens without verification (for claim extraction)
- Token validation with expiration checking
- Support for multiple token storage locations (federatedTokens, openidTokens)
- Integration with existing template variable system
- Comprehensive test suite with Cognito-specific scenarios
- Provider-agnostic design supporting Cognito, Azure AD, Auth0, etc.
Security:
- Server-side only token processing
- Automatic token expiration validation
- Graceful fallbacks for missing/invalid tokens
- No client-side token exposure
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Add federated token propagation to OIDC authentication strategies
Adds federatedTokens object to user during authentication to enable
federated provider token template variables in LibreChat configuration.
Changes:
- OpenID JWT Strategy: Extract raw JWT from Authorization header and
attach as federatedTokens.access_token to enable {{LIBRECHAT_OPENID_TOKEN}}
placeholder resolution
- OpenID Strategy: Attach tokenset tokens as federatedTokens object to
standardize token access across both authentication strategies
This enables proper token propagation for custom endpoints and MCP
servers that require federated provider tokens for authorization.
Resolves missing token issue reported by @ramden in PR #9931🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Denis Ramic <denis.ramic@nfon.com>
Co-Authored-By: Claude <noreply@anthropic.com>
* test: Add federatedTokens validation tests for OIDC strategies
Adds comprehensive test coverage for the federated token propagation
feature implemented in the authentication strategies.
Tests added:
- Verify federatedTokens object is attached to user with correct structure
(access_token, refresh_token, expires_at)
- Verify both tokenset and federatedTokens are present in user object
- Ensure tokens from OIDC provider are correctly propagated
Also fixes existing test suite by adding missing mocks:
- isEmailDomainAllowed function mock
- findOpenIDUser function mock
These tests validate the fix from commit 5874ba29f that enables
{{LIBRECHAT_OPENID_TOKEN}} template variable functionality.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: Remove implementation documentation file
The PR description already contains all necessary implementation details.
This documentation file is redundant and was requested to be removed.
* fix: skip s256 check
* fix(openid): handle missing refresh token in Cognito token refresh response
When OPENID_REUSE_TOKENS=true, the token refresh flow was failing because
Cognito (and most OAuth providers) don't return a new refresh token in the
refresh grant response - they only return new access and ID tokens.
Changes:
- Modified setOpenIDAuthTokens() to accept optional existingRefreshToken parameter
- Updated validation to only require access_token (refresh_token now optional)
- Added logic to reuse existing refresh token when not provided in tokenset
- Updated refreshController to pass original refresh token as fallback
- Added comments explaining standard OAuth 2.0 refresh token behavior
This fixes the "Token is not present. User is not authenticated." error that
occurred during silent token refresh with Cognito as the OpenID provider.
Fixes: Authentication loop with OPENID_REUSE_TOKENS=true and AWS Cognito
* fix(openid): extract refresh token from cookies for template variable replacement
When OPENID_REUSE_TOKENS=true, the openIdJwtStrategy populates user.federatedTokens
to enable template variable replacement (e.g., {{LIBRECHAT_OPENID_ACCESS_TOKEN}}).
However, the refresh_token field was incorrectly sourced from payload.refresh_token,
which is always undefined because:
1. JWTs don't contain refresh tokens in their payload
2. The JWT itself IS the access token
3. Refresh tokens are separate opaque tokens stored in HTTP-only cookies
This caused extractOpenIDTokenInfo() to receive incomplete federatedTokens,
resulting in template variables remaining unreplaced in headers.
**Root Cause:**
- Line 90: `refresh_token: payload.refresh_token` (always undefined)
- JWTs only contain access token data in their claims
- Refresh tokens are separate, stored securely in cookies
**Solution:**
- Import `cookie` module to parse cookies from request
- Extract refresh token from `refreshToken` cookie
- Populate federatedTokens with both access token (JWT) and refresh token (from cookie)
**Impact:**
- Template variables like {{LIBRECHAT_OPENID_ACCESS_TOKEN}} now work correctly
- Headers in librechat.yaml are properly replaced with actual tokens
- MCP server authentication with federated tokens now functional
**Technical Details:**
- passReqToCallback=true in JWT strategy provides req object access
- Refresh token extracted via cookies.parse(req.headers.cookie).refreshToken
- Falls back gracefully if cookie header or refreshToken is missing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: re-resolve headers on each request to pick up fresh federatedTokens
- OpenAIClient now re-resolves headers in chatCompletion() before each API call
- This ensures template variables like {{LIBRECHAT_OPENID_TOKEN}} are replaced
with actual token values from req.user.federatedTokens
- initialize.js now stores original template headers instead of pre-resolved ones
- Fixes template variable replacement when OPENID_REUSE_TOKENS=true
The issue was that headers were only resolved once during client initialization,
before openIdJwtStrategy had populated user.federatedTokens. Now headers are
re-resolved on every request with the current user's fresh tokens.
* debug: add logging to track header resolution in OpenAIClient
* debug: log tokenset structure after refresh to diagnose missing access_token
* fix: set federatedTokens on user object after OAuth refresh
- After successful OAuth token refresh, the user object was not being
updated with federatedTokens
- This caused template variable resolution to fail on subsequent requests
- Now sets user.federatedTokens with access_token, id_token, refresh_token
and expires_at from the refreshed tokenset
- Fixes template variables like {{LIBRECHAT_OPENID_TOKEN}} not being
replaced after token refresh
- Related to PR #9931 (OpenID federated token support)
* fix(openid): pass user object through agent chain for template variable resolution
Root cause: buildAgentContext in agents/run.ts called resolveHeaders without
the user parameter, preventing OpenID federated token template variables from
being resolved in agent runtime parameters.
Changes:
- packages/api/src/agents/run.ts: Add user parameter to createRun signature
- packages/api/src/agents/run.ts: Pass user to resolveHeaders in buildAgentContext
- api/server/controllers/agents/client.js: Pass user when calling createRun
- api/server/services/Endpoints/bedrock/options.js: Add resolveHeaders call with debug logging
- api/server/services/Endpoints/custom/initialize.js: Add debug logging
- packages/api/src/utils/env.ts: Add comprehensive debug logging and stack traces
- packages/api/src/utils/oidc.ts: Fix eslint errors (unused type, explicit any)
This ensures template variables like {{LIBRECHAT_OPENID_TOKEN}} and
{{LIBRECHAT_USER_OPENIDID}} are properly resolved in both custom endpoint
headers and Bedrock AgentCore runtime parameters.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: remove debug logging from OpenID token template feature
Removed excessive debug logging that was added during development to make
the PR more suitable for upstream review:
- Removed 7 debug statements from OpenAIClient.js
- Removed all console.log statements from packages/api/src/utils/env.ts
- Removed debug logging from bedrock/options.js
- Removed debug logging from custom/initialize.js
- Removed debug statement from AuthController.js
This reduces the changeset by ~50 lines while maintaining full functionality
of the OpenID federated token template variable feature.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* test(openid): add comprehensive unit tests for template variable substitution
- Add 34 unit tests for OIDC token utilities (oidc.spec.ts)
- Test coverage for token extraction, validation, and placeholder processing
- Integration tests for full OpenID token flow
- All tests pass with comprehensive edge case coverage
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
* test: fix OpenID federated tokens test failures
- Add serverMetadata() mock to openid-client mock configuration
* Fixes TypeError in openIdJwtStrategy.js where serverMetadata() was being called
* Mock now returns jwks_uri and end_session_endpoint as expected by the code
- Update outdated initialize.spec.js test
* Remove test expecting resolveHeaders call during initialization
* Header resolution was refactored to be deferred until LLM request time
* Update test to verify options are returned correctly with useLegacyContent flag
Fixes#9931 CI failures for backend unit tests
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: fix package-lock.json conflict
* chore: sync package-log with upstream
* chore: cleanup
* fix: use createSafeUser
* fix: fix createSafeUser signature
* chore: remove comments
* chore: purge comments
* fix: update Jest testPathPattern to testPathPatterns for Jest 30+ compatibility
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Denis Ramic <denis.ramic@nfon.com>
Co-authored-by: kristjanaapro <kristjana@apro.is>
chore: import order and add back JSDoc for OpenID JWT callback
* add use of immutable claims to identify user object
* fix semicolons
* update email attribute on change
* replace ternary expressions
* fix semicolon
* chore: add typing
* chore: reorder fields in `findOpenIDUser`
* refactor: optimize user lookup logic in `findOpenIDUser` function to minimize database roundtrips
* refactor: integrate findOpenIDUser for improved user retrieval in refreshController
* refactor: improve error logging for invalid refresh tokens in refreshController
* ci: mock findUser correctly in openidStrategy tests
* test: add unit tests for findOpenIDUser function to enhance user retrieval logic
---------
Co-authored-by: Joachim Keltsch <joachim.keltsch@daimlertruck.com>
* 🔧 fix: Enhance OpenID token handling with user ID for image path validation
* 🔧 fix: Change logger level to error for user info fetch failure and remove redundant info log in OpenID user lookup
* 🔧 refactor: Remove validateImageRequest from middleware exports and enhance validation logic in validateImageRequest.js
* Removed validateImageRequest from the middleware index.
* Improved error handling and validation checks in validateImageRequest.js, including handling of OpenID tokens, URL length, and malformed URLs.
* Updated tests in validateImages.spec.js to cover new validation scenarios and edge cases.
* refactor: `packages/api` build scripts for better inline debugging
* refactor: Explicitly select secure fields as no longer returned by default, exclude backupCodes from user data retrieval in authentication and 2FA processes
* refactor: Backup Codes UI to not expect backup codes, only regeneration
* refactor: Ensure secure fields are deleted from user data in getUserController
* feat: Add configurable retention period for temporary chats
* Addressing eslint errors
* Fix: failing test due to missing registration
* Update: variable name and use hours instead of days for chat retention
* Addressing comments
* chore: fix import order in Conversation.js
* chore: import order in Message.js
* chore: fix import order in config.ts
* chore: move common methods to packages/api to reduce potential for circular dependencies
* refactor: update temp chat retention config type to Partial<TCustomConfig>
* refactor: remove unused config variable from AppService and update loadCustomConfig tests with logger mock
* refactor: handle model undefined edge case by moving Session model initialization inside methods
---------
Co-authored-by: Rakshit Tiwari <rak1729e@gmail.com>
* refactor: move model definitions and database-related methods to packages/data-schemas
* ci: update tests due to new DB structure
fix: disable mocking `librechat-data-provider`
feat: Add schema exports to data-schemas package
- Introduced a new schema module that exports various schemas including action, agent, and user schemas.
- Updated index.ts to include the new schema exports for better modularity and organization.
ci: fix appleStrategy tests
fix: Agent.spec.js
ci: refactor handleTools tests to use MongoMemoryServer for in-memory database
fix: getLogStores imports
ci: update banViolation tests to use MongoMemoryServer and improve session mocking
test: refactor samlStrategy tests to improve mock configurations and user handling
ci: fix crypto mock in handleText tests for improved accuracy
ci: refactor spendTokens tests to improve model imports and setup
ci: refactor Message model tests to use MongoMemoryServer and improve database interactions
* refactor: streamline IMessage interface and move feedback properties to types/message.ts
* refactor: use exported initializeRoles from `data-schemas`, remove api workspace version (this serves as an example of future migrations that still need to happen)
* refactor: update model imports to use destructuring from `~/db/models` for consistency and clarity
* refactor: remove unused mongoose imports from model files for cleaner code
* refactor: remove unused mongoose imports from Share, Prompt, and Transaction model files for cleaner code
* refactor: remove unused import in Transaction model for cleaner code
* ci: update deploy workflow to reference new Docker Dev Branch Images Build and add new workflow for building Docker images on dev branch
* chore: cleanup imports
* feat: integrate OpenID Connect support with token reuse
- Added `jwks-rsa` and `new-openid-client` dependencies for OpenID Connect functionality.
- Implemented OpenID token refresh logic in `AuthController`.
- Enhanced `LogoutController` to handle OpenID logout and session termination.
- Updated JWT authentication middleware to support OpenID token provider.
- Modified OAuth routes to accommodate OpenID authentication and token management.
- Created `setOpenIDAuthTokens` function to manage OpenID tokens in cookies.
- Upgraded OpenID strategy with user info fetching and token exchange protocol.
- Introduced `openIdJwtLogin` strategy for handling OpenID JWT tokens.
- Added caching mechanism for exchanged OpenID tokens.
- Updated configuration to include OpenID exchanged tokens cache key.
- updated .env.example to include the new env variables needed for the feature.
* fix: update return type in downloadImage documentation for clarity and fixed openIdJwtLogin env variables
* fix: update Jest configuration and tests for OpenID strategy integration
* fix: update OpenID strategy to include callback URL in setup
* fix: fix optionalJwtAuth middleware to support OpenID token reuse and improve currentUrl method in CustomOpenIDStrategy to override the dynamic host issue related to proxy (e.g. cloudfront)
* fix: fixed code formatting
* Fix: Add mocks for openid-client and passport strategy in Jest configuration to fix unit tests
* fix eslint errors: Format mock file openid-client.
* ✨ feat: Add PKCE support for OpenID and default handling in strategy setup
---------
Co-authored-by: Atef Bellaaj <slalom.bellaaj@external.daimlertruck.com>
Co-authored-by: Ruben Talstra <RubenTalstra1211@outlook.com>
* 🔒 feat: add Two-Factor Authentication (2FA) with backup codes & QR support (#5684)
* working version for generating TOTP and authenticate.
* better looking UI
* refactored + better TOTP logic
* fixed issue with UI
* fixed issue: remove initial setup when closing window before completion.
* added: onKeyDown for verify and disable
* refactored some code and cleaned it up a bit.
* refactored some code and cleaned it up a bit.
* refactored some code and cleaned it up a bit.
* refactored some code and cleaned it up a bit.
* fixed issue after updating to new main branch
* updated example
* refactored controllers
* removed `passport-totp` not used.
* update the generateBackupCodes function to generate 10 codes by default:
* update the backup codes to an object.
* fixed issue with backup codes not working
* be able to disable 2FA with backup codes.
* removed new env. replaced with JWT_SECRET
* ✨ style: improved a11y and style for TwoFactorAuthentication
* 🔒 fix: small types checks
* ✨ feat: improve 2FA UI components
* fix: remove unnecessary console log
* add option to disable 2FA with backup codes
* - add option to refresh backup codes
- (optional) maybe show the user which backup codes have already been used?
* removed text to be able to merge the main.
* removed eng tx to be able to merge
* fix: migrated lang to new format.
* feat: rewrote whole 2FA UI + refactored 2FA backend
* chore: resolving conflicts
* chore: resolving conflicts
* fix: missing packages, because of resolving conflicts.
* fix: UI issue and improved a11y
* fix: 2FA backup code not working
* fix: update localization keys for UI consistency
* fix: update button label to use localized text
* fix: refactor backup codes regeneration and update localization keys
* fix: remove outdated translation for shared links management
* fix: remove outdated 2FA code prompts from translation.json
* fix: add cursor styles for backup codes item based on usage state
* fix: resolve conflict issue
* fix: resolve conflict issue
* fix: resolve conflict issue
* fix: missing packages in package-lock.json
* fix: add disabled opacity to the verify button in TwoFactorScreen
* ⚙ fix: update 2FA logic to rely on backup codes instead of TOTP status
* ⚙️ fix: Simplify user retrieval in 2FA logic by removing unnecessary TOTP secret query
* ⚙️ test: Add unit tests for TwoFactorAuthController and twoFactorControllers
* ⚙️ fix: Ensure backup codes are validated as an array before usage in 2FA components
* ⚙️ fix: Update module path mappings in tests to use relative paths
* ⚙️ fix: Update moduleNameMapper in jest.config.js to remove the caret from path mapping
* ⚙️ refactor: Simplify import paths in TwoFactorAuthController and twoFactorControllers test files
* ⚙️ test: Mock twoFactorService methods in twoFactorControllers tests
* ⚙️ refactor: Comment out unused imports and mock setups in test files for two-factor authentication
* ⚙️ refactor: removed files
* refactor: Exclude totpSecret from user data retrieval in AuthController, LoginController, and jwtStrategy
* refactor: Consolidate backup code verification to apply DRY and remove default array in user schema
* refactor: Enhance two-factor authentication ux/flow with improved error handling and loading state management, prevent redirect to /login
---------
Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
* 🔒 fix: update refresh token handling to use plain token instead of hashed token
* 🔒 fix: simplify logoutUser by using plain refresh token for session lookup
* ✨ feat: Implement session management with CRUD operations and integrate into user workflows
* ✨ refactor: Update session model import paths and enhance session creation logic in AuthService
* ✨ refactor: Validate session and user ID formats in session management functions
* ✨ style: Enhance UI components with improved styling and accessibility features
* chore: Update login form tests to use getByTestId instead of getByRole, remove console.log()
* chore: Update login form tests to use getByTestId instead of getByRole
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* feat: verification email
* chore: email verification invalid; localize: update
* fix: redirect to login when signup: fix: save emailVerified correctly
* docs: update ALLOW_UNVERIFIED_EMAIL_LOGIN; fix: don't accept login only when ALLOW_UNVERIFIED_EMAIL_LOGIN = true
* fix: user needs to be authenticated
* style: update
* fix: registration success message and redirect logic
* refactor: use `isEnabled` in ALLOW_UNVERIFIED_EMAIL_LOGIN
* refactor: move checkEmailConfig to server/utils
* refactor: use req as param for verifyEmail function
* chore: jsdoc
* chore: remove console log
* refactor: rename `createNewUser` to `createSocialUser`
* refactor: update typing and add expiresAt field to userSchema
* refactor: begin use of user methods over direct model access for User
* refactor: initial email verification rewrite
* chore: typing
* refactor: registration flow rewrite
* chore: remove help center text
* refactor: update getUser to getUserById and add findUser methods. general fixes from recent changes
* refactor: Update updateUser method to remove expiresAt field and use $set and $unset operations, createUser now returns Id only
* refactor: Update openidStrategy to use optional chaining for avatar check, move saveBuffer init to buffer condition
* refactor: logout on deleteUser mutatation
* refactor: Update openidStrategy login success message format
* refactor: Add emailVerified field to Discord and Facebook profile details
* refactor: move limiters to separate middleware dir
* refactor: Add limiters for email verification and password reset
* refactor: Remove getUserController and update routes and controllers accordingly
* refactor: Update getUserById method to exclude password and version fields
* refactor: move verification to user route, add resend verification option
* refactor: Improve email verification process and resend option
* refactor: remove more direct model access of User and remove unused code
* refactor: replace user authentication methods and token generation
* fix: add user.id to jwt user
* refactor: Update AuthContext to include setError function, add resend link to Login Form, make registration redirect shorter
* fix(updateUserPluginsService): ensure userPlugins variable is defined
* refactor: Delete all shared links for a specific user
* fix: remove use of direct User.save() in handleExistingUser
* fix(importLibreChatConvo): handle missing createdAt field in messages
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* feat: password reset disable option; fix: account email leak
* fix(LoginSpec): typo
* test: fixed LoginForm test
* fix: disable password reset when undefined
* refactor: use a helper function
* fix: tests
* feat: Remove unused error message in password reset process
* chore: Update password reset email message
* refactor: only allow password reset if explicitly allowed
* feat: Add password reset email service configuration check
The code changes in `checks.js` add a new function `checkPasswordReset()` that checks if the email service is configured when password reset is enabled. If the email service is not configured, a warning message is logged. This change ensures secure password reset functionality by prompting the user to configure the email service.
Co-authored-by: Berry-13 <root@Berry>
Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
* chore: remove import order rules
* refactor: simplify password reset logic and align against Observable Response Discrepancy
* chore: make password reset warning more prominent
* chore(AuthService): better logging for password resets, refactor requestPasswordReset to use req object, fix sendEmail error when email config is not present
* refactor: fix styling of password reset email message
* chore: add missing type for passwordResetEnabled, TStartupConfig
* fix(LoginForm): prevent login form flickering
* fix(ci): Update login form to use mocked startupConfig for rendering correctly
* refactor: Improve password reset UI, applies DRY
* chore: Add logging to password reset validation middleware
* chore(CONTRIBUTING): Update import order conventions
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
Co-authored-by: Berry-13 <root@Berry>
Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
* WIP: initial logging changes
add several transports in ~/config/winston
omit messages in logs, truncate long strings
add short blurb in dotenv for debug logging
GoogleClient: using logger
OpenAIClient: using logger, handleOpenAIErrors
Adding typedef for payload message
bumped winston and using winston-daily-rotate-file
moved config for server paths to ~/config dir
Added `DEBUG_LOGGING=true` to .env.example
* WIP: Refactor logging statements in code
* WIP: Refactor logging statements and import configurations
* WIP: Refactor logging statements and import configurations
* refactor: broadcast Redis initialization message with `info` not `debug`
* refactor: complete Refactor logging statements and import configurations
* chore: delete unused tools
* fix: circular dependencies due to accessing logger
* refactor(handleText): handle booleans and write tests
* refactor: redact sensitive values, better formatting
* chore: improve log formatting, avoid passing strings to 2nd arg
* fix(ci): fix jest tests due to logger changes
* refactor(getAvailablePluginsController): cache plugins as they are static and avoids async addOpenAPISpecs call every time
* chore: update docs
* chore: update docs
* chore: create separate meiliSync logger, clean up logs to avoid being unnecessarily verbose
* chore: spread objects where they are commonly logged to allow string truncation
* chore: improve error log formatting
* chore: add back BrowserOp
* chore: make CI env and not DEV env generate refresh tokens every time
* chore: make 'CI' env var captilization uniform across the app
* chore: change NODE_ENV for playwright to
* fix(getLogStores): correct wrong prop passed to keyv opts: duration -> ttl
* fix: edge case where we get a blank screen if the initially intercepted 401 error is from a refresh token request; in this case, make explicit to the server that we are retrying from a refreshToken request