Skip to content

🔐 fix: Stop Deriving the Exchange Origin from the Referer on SSO Callbacks - #114

Open
dustinhealy wants to merge 5 commits into
mainfrom
fix/issue-79-sso-pkce-entraid
Open

🔐 fix: Stop Deriving the Exchange Origin from the Referer on SSO Callbacks#114
dustinhealy wants to merge 5 commits into
mainfrom
fix/issue-79-sso-pkce-entraid

Conversation

@dustinhealy

@dustinhealy dustinhealy commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes the Azure EntraID admin SSO failure where login succeeds at the IdP but the panel shows "Authentication failed / Authorization code has expired. Please try again." (401 from /api/admin/oauth/exchange).

When the OAuth exchange request reaches the panel without an Origin header (proxies or clients that drop it), getRequestOrigin fell back to the referer header, which identifies whatever page or IdP initiated the request rather than the panel itself; on an IdP-initiated callback that is https://login.microsoftonline.com. Separately, multi-hop proxy chains (the report runs TRUST_PROXY=3) produce comma-joined x-forwarded-proto values that parsed into an invalid origin. Either way the panel forwarded a wrong Origin to LibreChat, whose exchange-code origin binding rejects it with INVALID_OR_EXPIRED_CODE even though authentication succeeded.

The panel's own origin now resolves in a deterministic order: the new optional ADMIN_PANEL_PUBLIC_URL env var when set (an authoritative override for deployments whose proxies strip Origin and forward no metadata; mirror LibreChat's ADMIN_PANEL_URL), then the Origin header, then forwarding metadata: the first x-forwarded-host value wins over host (Host-rewriting proxies replace host with the internal upstream authority, e.g. admin-panel:3000, which would equally fail the origin binding), the first x-forwarded-proto value supplies the scheme, and when proto metadata is absent the scheme is recovered from the referer only when the referer host matches the serving host, so a foreign IdP referer can never be forwarded as the panel origin.

The "State present but no PKCE challenge found" warning in the reporter's LibreChat logs is a separate LibreChat-side defect: openid-client v6 overwrites the admin OAuth state with a random one when the IdP metadata does not advertise code_challenge_methods_supported (Azure omits it), orphaning the cached PKCE challenge. That degrades PKCE to unenforced but does not cause the 401; it needs an upstream fix in openidStrategy.js (pin a nonce when the IdP does not advertise PKCE so the state survives). Setting OPENID_GENERATE_NONCE=true on LibreChat is a working mitigation today.

Fixes #79

Change Type

  • Bug fix (non-breaking change which fixes an issue)

Testing

Regression tests drive the real oauthExchangeFn path: an exchange carrying an IdP referer and no Origin must forward the panel's serving origin (fails before the fix with https://login.microsoftonline.com), multi-value x-forwarded-proto must resolve to the first value, x-forwarded-host must win over a proxy-rewritten upstream host, multi-value x-forwarded-host must use the first value, a same-host https referer must recover the scheme when proto metadata is absent while a foreign referer must not, ADMIN_PANEL_PUBLIC_URL must win over all headers when set, and a malformed value must fall back to header derivation with a warning. Also verified end to end at runtime against a stub implementing LibreChat's exact exchange contract (one-time codes, origin binding, PKCE verification): with an IdP referer and no Origin, main forwards Origin: https://login.microsoftonline.com and fails with the exact user-visible expired-code message, while this branch forwards the panel's own origin and completes login, including the full browser SSO flow on both dev and production builds. The comma x-forwarded-proto case was likewise verified at runtime (main produced an unparseable origin and failed; this branch succeeds). Local gates: full vitest suite passes (803 tests), eslint clean, tsc --noEmit clean.

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes

…backs

When the OAuth exchange request reaches the panel without an Origin header (proxies or clients that drop it), the referer identifies the initiating page or IdP rather than the panel, and multi-hop proxy chains produce comma-joined x-forwarded-proto values that parse into an invalid origin. Either way LibreChat's exchange-code origin binding rejects the code with INVALID_OR_EXPIRED_CODE, shown as "Authorization code has expired. Please try again." despite successful IdP authentication.

Resolve the panel's own origin from the Origin header when present, otherwise from host plus the first x-forwarded-proto value, and never from the referer. Verified end to end against a stub implementing LibreChat's exchange contract: with an IdP referer and no Origin, main forwards the IdP origin and fails; this branch forwards the panel origin and completes login, including the full browser SSO flow.
@dustinhealy

Copy link
Copy Markdown
Contributor Author

@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: b155debc3b

ℹ️ 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 src/server/auth.ts Outdated
When a reverse proxy rewrites Host to the internal upstream authority (for example admin-panel:3000) while preserving the browser-visible authority in X-Forwarded-Host, the host fallback constructed an internal origin that LibreChat's exchange-code origin binding rejects. getRequestOrigin now takes the first x-forwarded-host value over host, mirroring the existing x-forwarded-proto handling.
@dustinhealy

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: fcca14cadd

ℹ️ 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".

@dustinhealy
dustinhealy marked this pull request as ready for review August 10, 2026 03:33
Comment thread src/server/auth.ts
…roto is absent

Defaulting the scheme to http when both the Origin header and x-forwarded-proto are missing broke HTTPS deployments whose proxy strips Origin without forwarding proto metadata. getRequestOrigin now uses the referer's origin in that case, but only when the referer host matches the serving host, so a foreign referer such as an IdP origin can never be forwarded as the panel's own origin.
@dustinhealy

Copy link
Copy Markdown
Contributor Author

@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: ca39b0073a

ℹ️ 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 src/server/auth.ts
…in override

Deployments behind proxies that strip the Origin header and forward no x-forwarded-proto or x-forwarded-host metadata had no deterministic way to satisfy LibreChat's exchange-code origin binding. When ADMIN_PANEL_PUBLIC_URL is set, getRequestOrigin now sends its origin directly, before any header derivation, and operators can mirror LibreChat's ADMIN_PANEL_URL value. The header chain (origin, forwarded host and proto, same-host referer) is unchanged as the fallback, and a malformed value is warned about and ignored.
@dustinhealy

Copy link
Copy Markdown
Contributor Author

@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: 5078f2cedb

ℹ️ 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 src/server/auth.ts Outdated
Comment thread src/server/auth.ts Outdated
A syntactically valid non-web ADMIN_PANEL_PUBLIC_URL such as file: or mailto: parses to origin null or an unusable scheme and would override valid request headers, so the override now requires an http or https protocol and a non-null origin before it wins. A serving authority carrying an explicit default port like example.com:443 failed the same-host referer comparison because URL strips default ports, so the comparison now strips the referer scheme's default port from the serving host first.
@dustinhealy

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 9cd1714b15

ℹ️ 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".

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Admin SSO PKCE issue still exists for Azure EntraID

1 participant