🛂 fix: Reject OpenID Email Fallback When Stored openidId Mismatches Token Sub#12312
Merged
Conversation
…oken sub When `findOpenIDUser` falls back to email lookup after the primary `openidId`/`idOnTheSource` query fails, it now rejects any user whose stored `openidId` differs from the incoming JWT subject claim. This closes an account-takeover vector where a valid IdP JWT containing a victim's email but a different `sub` could authenticate as the victim when OPENID_REUSE_TOKENS is enabled. The migration path (user has no `openidId` yet) is unaffected.
Update `findOpenIDUser` unit tests to assert that email-based lookups returning a user with a different `openidId` are rejected with AUTH_FAILED. Add matching integration test in `openIdJwtStrategy.spec` exercising the full verify callback with the real `findOpenIDUser`.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR tightens OpenID authentication by hardening the email-fallback path in findOpenIDUser, preventing a user record found by email from being accepted when its stored openidId (subject) doesn’t match the token subject.
Changes:
- Add a guard to reject email-fallback authentication when an existing user’s stored
openidIddiffers from the tokensub. - Update
findOpenIDUserunit tests to cover accept/reject behavior for matching vs mismatchedopenidIdon email fallback. - Add an integration-style spec in
openIdJwtStrategyto ensure mismatchedopenidIdvia email fallback fails login.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| packages/api/src/auth/openid.ts | Adds a rejection guard for email fallback when stored openidId mismatches token subject. |
| packages/api/src/auth/openid.spec.ts | Updates/extends unit tests to validate the new guard and expected outcomes. |
| api/strategies/openIdJwtStrategy.spec.js | Adds a strategy-level test ensuring mismatched subject via email fallback is rejected. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The `&& openidId` middle term in the guard condition caused it to be bypassed when the incoming token `sub` was empty or undefined. Since the JS callers can pass `payload?.sub` (which may be undefined), this created a path where the guard never fired and the email fallback returned the victim's account. Removing the term ensures the guard rejects whenever the stored openidId differs from the incoming value, regardless of whether the incoming value is falsy.
Add regression test for the guard bypass when `openidId` is an empty string and the email lookup finds a user with a stored openidId. Add integration test in openidStrategy.spec.js exercising the mismatch rejection through the full processOpenIDAuth callback, ensuring both OIDC paths (JWT reuse and standard callback) are covered. Restore intent-documenting comment on the no-provider fixture.
openidId Mismatches Token Sub
|
What is the point of doing the email fallback at all? Won't it always fail? |
jcbartle
pushed a commit
to jcbartle/LibreChat
that referenced
this pull request
May 11, 2026
… Token Sub (danny-avila#12312) * 🔐 fix: Reject OpenID email fallback when stored openidId mismatches token sub When `findOpenIDUser` falls back to email lookup after the primary `openidId`/`idOnTheSource` query fails, it now rejects any user whose stored `openidId` differs from the incoming JWT subject claim. This closes an account-takeover vector where a valid IdP JWT containing a victim's email but a different `sub` could authenticate as the victim when OPENID_REUSE_TOKENS is enabled. The migration path (user has no `openidId` yet) is unaffected. * test: Validate openidId mismatch guard in email fallback path Update `findOpenIDUser` unit tests to assert that email-based lookups returning a user with a different `openidId` are rejected with AUTH_FAILED. Add matching integration test in `openIdJwtStrategy.spec` exercising the full verify callback with the real `findOpenIDUser`. * 🔐 fix: Remove redundant `openidId` truthiness check from mismatch guard The `&& openidId` middle term in the guard condition caused it to be bypassed when the incoming token `sub` was empty or undefined. Since the JS callers can pass `payload?.sub` (which may be undefined), this created a path where the guard never fired and the email fallback returned the victim's account. Removing the term ensures the guard rejects whenever the stored openidId differs from the incoming value, regardless of whether the incoming value is falsy. * test: Cover falsy openidId bypass and openidStrategy mismatch rejection Add regression test for the guard bypass when `openidId` is an empty string and the email lookup finds a user with a stored openidId. Add integration test in openidStrategy.spec.js exercising the mismatch rejection through the full processOpenIDAuth callback, ensuring both OIDC paths (JWT reuse and standard callback) are covered. Restore intent-documenting comment on the no-provider fixture.
ThomasVuNguyen
pushed a commit
to ThomasVuNguyen/LibreChat
that referenced
this pull request
Jul 15, 2026
… Token Sub (danny-avila#12312) * 🔐 fix: Reject OpenID email fallback when stored openidId mismatches token sub When `findOpenIDUser` falls back to email lookup after the primary `openidId`/`idOnTheSource` query fails, it now rejects any user whose stored `openidId` differs from the incoming JWT subject claim. This closes an account-takeover vector where a valid IdP JWT containing a victim's email but a different `sub` could authenticate as the victim when OPENID_REUSE_TOKENS is enabled. The migration path (user has no `openidId` yet) is unaffected. * test: Validate openidId mismatch guard in email fallback path Update `findOpenIDUser` unit tests to assert that email-based lookups returning a user with a different `openidId` are rejected with AUTH_FAILED. Add matching integration test in `openIdJwtStrategy.spec` exercising the full verify callback with the real `findOpenIDUser`. * 🔐 fix: Remove redundant `openidId` truthiness check from mismatch guard The `&& openidId` middle term in the guard condition caused it to be bypassed when the incoming token `sub` was empty or undefined. Since the JS callers can pass `payload?.sub` (which may be undefined), this created a path where the guard never fired and the email fallback returned the victim's account. Removing the term ensures the guard rejects whenever the stored openidId differs from the incoming value, regardless of whether the incoming value is falsy. * test: Cover falsy openidId bypass and openidStrategy mismatch rejection Add regression test for the guard bypass when `openidId` is an empty string and the email lookup finds a user with a stored openidId. Add integration test in openidStrategy.spec.js exercising the mismatch rejection through the full processOpenIDAuth callback, ensuring both OIDC paths (JWT reuse and standard callback) are covered. Restore intent-documenting comment on the no-provider fixture.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes an account-takeover vector in the OpenID email-fallback authentication path. When the primary
openidId/idOnTheSourcelookup fails andfindOpenIDUserfalls back to an email-based query, a JWT carrying a victim's email address but a differentsubclaim could previously authenticate as the victim — provided the email lookup returned a result andOPENID_REUSE_TOKENSwas enabled.findOpenIDUser(packages/api/src/auth/openid.ts) that rejects any email-matched user whose storedopenidIddiffers from the incoming tokensub, returningAUTH_FAILEDimmediately.openidIdyet (legitimate migration candidates) are unaffected.&& openidIdmiddle term from the guard condition that would have silently bypassed the check when the incomingsubwas an empty string orundefined, as can occur from JavaScript callers passingpayload?.sub.openid.spec.tswhose expectations reflected the previous broken behavior (returning the matched user rather thanAUTH_FAILED).openidIdbypass path:openidId: ''with an email lookup returning a user with a storedopenidIdnow correctly yieldsAUTH_FAILED.openIdJwtStrategy.spec.jsexercising the full JWT strategy verify callback with the realfindOpenIDUser, assertinguser === falseandinfo.message === 'auth_failed'on mismatch.openidStrategy.spec.jsexercising the mismatch rejection through the fullprocessOpenIDAuthcallback, confirmingcreateUserandupdateUserare never called on rejection.providerfixture to distinguish it from the provider-mismatch branch.Change Type
Testing
All three modified files have direct test coverage for the new guard. Tests use real function logic per project convention — no stub replacements of
findOpenIDUser.packages/api/src/auth/openid.spec.ts(unit —findOpenIDUserdirectly):should reject email fallback when existing openidId does not match token sub— mismatchedopenidIdreturnsAUTH_FAILEDshould allow email fallback when existing openidId matches token sub— matchingopenidIdreturns the usershould reject when user already has a different openidId— pre-existing mismatch scenarioshould reject when user has no provider but a different openidId— no-provider variantshould reject email fallback when openidId is empty and user has a stored openidId— falsysubbypass regressionapi/strategies/openIdJwtStrategy.spec.js(integration — JWT reuse path):should reject login when email fallback finds user with mismatched openidId— full verify callback with realfindOpenIDUser, mockedfindUserapi/strategies/openidStrategy.spec.js(integration — standard OIDC callback path):should block login when email fallback finds user with mismatched openidId— fullprocessOpenIDAuthflow, assertscreateUser/updateUsernot calledTest Configuration
All 121 tests pass (20 unit + 15 JWT strategy + 86 OpenID strategy).
Checklist