Skip to content

🔐 fix: Strip code_challenge from Admin OAuth requests before Passport#12534

Merged
danny-avila merged 5 commits into
devfrom
fix/admin-oauth-code-challenge
Apr 3, 2026
Merged

🔐 fix: Strip code_challenge from Admin OAuth requests before Passport#12534
danny-avila merged 5 commits into
devfrom
fix/admin-oauth-code-challenge

Conversation

@danny-avila

Copy link
Copy Markdown
Owner

openid-client v6's Passport Strategy uses currentUrl.searchParams.size === 0 to distinguish initial authorization requests from OAuth callbacks. The admin-panel-specific code_challenge query parameter caused the strategy to misclassify the request as a callback and return 401 Unauthorized.

openid-client v6's Passport Strategy uses `currentUrl.searchParams.size === 0`
to distinguish initial authorization requests from OAuth callbacks. The
admin-panel-specific `code_challenge` query parameter caused the strategy to
misclassify the request as a callback and return 401 Unauthorized.
Copilot AI review requested due to automatic review settings April 2, 2026 20:06
@danny-avila danny-avila changed the title 🔐 fix: Strip code_challenge from admin OAuth requests before Passport 🔐 fix: Strip code_challenge from Admin OAuth requests before Passport Apr 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes admin OAuth initiation requests being misclassified as OAuth callbacks by Passport/openid-client v6 when the admin panel adds a code_challenge query parameter.

Changes:

  • Added a helper to strip code_challenge from the Express request query/URL before invoking Passport strategies.
  • Applied the helper across all admin OAuth initiation routes (OpenID, SAML, Google, GitHub, Discord, Facebook, Apple).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread api/server/routes/admin/auth.js Outdated
Comment on lines +91 to +93
delete req.query.code_challenge;
req.originalUrl = req.originalUrl.replace(/[?&]code_challenge=[^&]+/, '');
req.url = req.url.replace(/[?&]code_challenge=[^&]+/, '');

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

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

This replace() can yield an invalid URL when code_challenge is the first query param but others follow (e.g. ?code_challenge=...&foo=1 becomes &foo=1). Prefer rebuilding req.originalUrl via URL/URLSearchParams (delete the param, then serialize) so separators (?/&) stay correct.

Suggested change
delete req.query.code_challenge;
req.originalUrl = req.originalUrl.replace(/[?&]code_challenge=[^&]+/, '');
req.url = req.url.replace(/[?&]code_challenge=[^&]+/, '');
const stripCodeChallengeFromUrl = (value) => {
const parsedUrl = new URL(value, 'http://localhost');
parsedUrl.searchParams.delete('code_challenge');
return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
};
delete req.query.code_challenge;
req.originalUrl = stripCodeChallengeFromUrl(req.originalUrl);
req.url = stripCodeChallengeFromUrl(req.url);

Copilot uses AI. Check for mistakes.
Comment thread api/server/routes/admin/auth.js Outdated
Comment on lines +91 to +93
delete req.query.code_challenge;
req.originalUrl = req.originalUrl.replace(/[?&]code_challenge=[^&]+/, '');
req.url = req.url.replace(/[?&]code_challenge=[^&]+/, '');

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

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

Same issue as above for req.url: stripping ?code_challenge=... via regex can leave a leading & when other params follow, producing a malformed URL string. Use URLSearchParams deletion + re-serialization (or equivalent) rather than a single regex replace.

Suggested change
delete req.query.code_challenge;
req.originalUrl = req.originalUrl.replace(/[?&]code_challenge=[^&]+/, '');
req.url = req.url.replace(/[?&]code_challenge=[^&]+/, '');
const removeCodeChallengeFromUrl = (url) => {
const hashIndex = url.indexOf('#');
const beforeHash = hashIndex === -1 ? url : url.slice(0, hashIndex);
const hash = hashIndex === -1 ? '' : url.slice(hashIndex);
const queryIndex = beforeHash.indexOf('?');
if (queryIndex === -1) {
return url;
}
const path = beforeHash.slice(0, queryIndex);
const search = beforeHash.slice(queryIndex + 1);
const searchParams = new URLSearchParams(search);
searchParams.delete('code_challenge');
const serializedSearch = searchParams.toString();
return `${path}${serializedSearch ? `?${serializedSearch}` : ''}${hash}`;
};
delete req.query.code_challenge;
req.originalUrl = removeCodeChallengeFromUrl(req.originalUrl);
req.url = removeCodeChallengeFromUrl(req.url);

Copilot uses AI. Check for mistakes.
openid-client v6's Passport Strategy uses `currentUrl.searchParams.size === 0`
to distinguish initial authorization requests from OAuth callbacks. The
admin-panel-specific `code_challenge` query parameter caused the strategy to
misclassify the request as a callback and return 401 Unauthorized.

- Fix regex to handle `code_challenge` in any query position without producing
  malformed URLs, and handle empty `code_challenge=` values (`[^&]*` vs `[^&]+`)
- Combine `storePkceChallenge` + `stripCodeChallenge` into a single
  `storeAndStripChallenge` helper to enforce read-store-strip ordering
- Apply defensively to all 7 admin OAuth providers
- Add 12 unit tests covering stripCodeChallenge and storeAndStripChallenge
- Move stripCodeChallenge and storeAndStripChallenge to
  api/server/utils/adminPkce.js — eliminates _test production export
  and avoids loading the full auth.js module tree in tests
- Add missing req.originalUrl/req.url assertions to invalid-challenge
  and no-challenge test branches (regression blind spots)
- Hoist cache reference to module scope in tests (was redundantly
  re-acquired from mock factory on every beforeEach)
@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

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

- Fix import order in auth.js (longest-to-shortest per CLAUDE.md)
- Remove unused PKCE_CHALLENGE_TTL/PKCE_CHALLENGE_PATTERN exports
- Hoist strip arrow to module-scope stripChallengeFromUrl
- Rename auth.test.js → auth.spec.js (project convention)
- Tighten cache-failure test: toBe instead of toContain, add req.url
Move stripCodeChallenge and storeAndStripChallenge from api/server/utils
into packages/api/src/auth/exchange.ts alongside the existing PKCE
verification logic. Cache is now injected as a Keyv parameter, matching
the dependency-injection pattern used throughout packages/api/.

- Add PkceStrippableRequest interface for minimal req typing
- auth.js imports storeAndStripChallenge from @librechat/api
- Delete api/server/utils/adminPkce.js
- Move tests to packages/api/src/auth/adminPkce.spec.ts (TypeScript,
  real Keyv instances, no getLogStores mock needed)
@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

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

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

@danny-avila
danny-avila merged commit fa4a43d into dev Apr 3, 2026
10 checks passed
@danny-avila
danny-avila deleted the fix/admin-oauth-code-challenge branch April 3, 2026 01:03
jcbartle pushed a commit to jcbartle/LibreChat that referenced this pull request May 11, 2026
…rt (danny-avila#12534)

* 🔐 fix: Strip code_challenge from admin OAuth requests before Passport

openid-client v6's Passport Strategy uses `currentUrl.searchParams.size === 0`
to distinguish initial authorization requests from OAuth callbacks. The
admin-panel-specific `code_challenge` query parameter caused the strategy to
misclassify the request as a callback and return 401 Unauthorized.

* 🔐 fix: Strip code_challenge from admin OAuth requests before Passport

openid-client v6's Passport Strategy uses `currentUrl.searchParams.size === 0`
to distinguish initial authorization requests from OAuth callbacks. The
admin-panel-specific `code_challenge` query parameter caused the strategy to
misclassify the request as a callback and return 401 Unauthorized.

- Fix regex to handle `code_challenge` in any query position without producing
  malformed URLs, and handle empty `code_challenge=` values (`[^&]*` vs `[^&]+`)
- Combine `storePkceChallenge` + `stripCodeChallenge` into a single
  `storeAndStripChallenge` helper to enforce read-store-strip ordering
- Apply defensively to all 7 admin OAuth providers
- Add 12 unit tests covering stripCodeChallenge and storeAndStripChallenge

* refactor: Extract PKCE helpers to utility file, harden tests

- Move stripCodeChallenge and storeAndStripChallenge to
  api/server/utils/adminPkce.js — eliminates _test production export
  and avoids loading the full auth.js module tree in tests
- Add missing req.originalUrl/req.url assertions to invalid-challenge
  and no-challenge test branches (regression blind spots)
- Hoist cache reference to module scope in tests (was redundantly
  re-acquired from mock factory on every beforeEach)

* chore: Address review NITs — imports, exports, naming, assertions

- Fix import order in auth.js (longest-to-shortest per CLAUDE.md)
- Remove unused PKCE_CHALLENGE_TTL/PKCE_CHALLENGE_PATTERN exports
- Hoist strip arrow to module-scope stripChallengeFromUrl
- Rename auth.test.js → auth.spec.js (project convention)
- Tighten cache-failure test: toBe instead of toContain, add req.url

* refactor: Move PKCE helpers to packages/api with dependency injection

Move stripCodeChallenge and storeAndStripChallenge from api/server/utils
into packages/api/src/auth/exchange.ts alongside the existing PKCE
verification logic. Cache is now injected as a Keyv parameter, matching
the dependency-injection pattern used throughout packages/api/.

- Add PkceStrippableRequest interface for minimal req typing
- auth.js imports storeAndStripChallenge from @librechat/api
- Delete api/server/utils/adminPkce.js
- Move tests to packages/api/src/auth/adminPkce.spec.ts (TypeScript,
  real Keyv instances, no getLogStores mock needed)
ThomasVuNguyen pushed a commit to ThomasVuNguyen/LibreChat that referenced this pull request Jul 15, 2026
…rt (danny-avila#12534)

* 🔐 fix: Strip code_challenge from admin OAuth requests before Passport

openid-client v6's Passport Strategy uses `currentUrl.searchParams.size === 0`
to distinguish initial authorization requests from OAuth callbacks. The
admin-panel-specific `code_challenge` query parameter caused the strategy to
misclassify the request as a callback and return 401 Unauthorized.

* 🔐 fix: Strip code_challenge from admin OAuth requests before Passport

openid-client v6's Passport Strategy uses `currentUrl.searchParams.size === 0`
to distinguish initial authorization requests from OAuth callbacks. The
admin-panel-specific `code_challenge` query parameter caused the strategy to
misclassify the request as a callback and return 401 Unauthorized.

- Fix regex to handle `code_challenge` in any query position without producing
  malformed URLs, and handle empty `code_challenge=` values (`[^&]*` vs `[^&]+`)
- Combine `storePkceChallenge` + `stripCodeChallenge` into a single
  `storeAndStripChallenge` helper to enforce read-store-strip ordering
- Apply defensively to all 7 admin OAuth providers
- Add 12 unit tests covering stripCodeChallenge and storeAndStripChallenge

* refactor: Extract PKCE helpers to utility file, harden tests

- Move stripCodeChallenge and storeAndStripChallenge to
  api/server/utils/adminPkce.js — eliminates _test production export
  and avoids loading the full auth.js module tree in tests
- Add missing req.originalUrl/req.url assertions to invalid-challenge
  and no-challenge test branches (regression blind spots)
- Hoist cache reference to module scope in tests (was redundantly
  re-acquired from mock factory on every beforeEach)

* chore: Address review NITs — imports, exports, naming, assertions

- Fix import order in auth.js (longest-to-shortest per CLAUDE.md)
- Remove unused PKCE_CHALLENGE_TTL/PKCE_CHALLENGE_PATTERN exports
- Hoist strip arrow to module-scope stripChallengeFromUrl
- Rename auth.test.js → auth.spec.js (project convention)
- Tighten cache-failure test: toBe instead of toContain, add req.url

* refactor: Move PKCE helpers to packages/api with dependency injection

Move stripCodeChallenge and storeAndStripChallenge from api/server/utils
into packages/api/src/auth/exchange.ts alongside the existing PKCE
verification logic. Cache is now injected as a Keyv parameter, matching
the dependency-injection pattern used throughout packages/api/.

- Add PkceStrippableRequest interface for minimal req typing
- auth.js imports storeAndStripChallenge from @librechat/api
- Delete api/server/utils/adminPkce.js
- Move tests to packages/api/src/auth/adminPkce.spec.ts (TypeScript,
  real Keyv instances, no getLogStores mock needed)
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.

2 participants