Skip to content

fix(auth): derive OAuth callback URL from request origin to prevent session loss#6912

Open
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1781719640-fix-oauth-callback-domain
Open

fix(auth): derive OAuth callback URL from request origin to prevent session loss#6912
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1781719640-fix-oauth-callback-domain

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Context

OAuth login (Google/GitHub/GitLab) fails when initiated from us.infisical.com because:

  1. The session cookie storing OAuth state/PKCE is scoped to us.infisical.com
  2. The OAuth callback URL is statically set to ${SITE_URL}/api/v1/sso/<provider> (resolving to app.infisical.com)
  3. When the provider redirects back to app.infisical.com, the session cookie is not sent (different domain)
  4. Passport state verification fails → user lands on /login/provider/error

This affects the VS Code extension (which opens us.infisical.com/login?callback_port=PORT) and any user on us.infisical.com using OAuth.

Fix: Derive the callback URL from the request's protocol and hostname instead of the static SITE_URL. This ensures the OAuth provider redirects back to the same domain the user started from, keeping the session cookie in scope.

const getOAuthCallbackUrl = (req: { protocol: string; hostname: string }, path: string) =>
  `${req.protocol}://${req.hostname}${path}`;

The dynamic callbackURL is passed to passport.authenticate() in both the redirect and callback phases for all three providers (Google, GitHub, GitLab). Post-login redirects also use the request origin.

Note: The OAuth apps in Google/GitHub/GitLab developer consoles must have both us.infisical.com and app.infisical.com callback URLs registered for this to work.

Steps to verify the change

  1. Navigate to us.infisical.com/login (or use the VS Code extension with US Region)
  2. Click Google/GitHub/GitLab OAuth login
  3. Complete authentication at the provider
  4. Verify you are redirected back to us.infisical.com/login/select-organization (not app.infisical.com)
  5. Verify the login completes successfully (no /login/provider/error redirect)

Type

  • Fix
  • Feature
  • Improvement
  • Breaking
  • Docs
  • Chore

Checklist

  • Title follows the conventional commit format: type(scope): short description (scope is optional, e.g., fix: prevent crash on sync or fix(api): handle null response).
  • Tested locally
  • Updated docs (if needed)
  • Updated CLAUDE.md files (if needed)
  • Read the contributing guide

Link to Devin session: https://app.devin.ai/sessions/919c400b80094b71b80c37b22bcaf540

…ession loss

The OAuth callback URL was statically set to SITE_URL (app.infisical.com), but
users initiating login from us.infisical.com had their session cookie scoped to
that domain. When the OAuth provider redirected back to app.infisical.com, the
session cookie was not sent, causing state/PKCE verification to fail.

Now the callback URL is derived from the request's protocol and hostname so
that the OAuth provider redirects back to the same domain the user started from,
keeping the session cookie in scope.

Co-Authored-By: jake <jake@infisical.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@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: 825e25521b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +46 to +47
const getOAuthCallbackUrl = (req: { protocol: string; hostname: string }, path: string) =>
`${req.protocol}://${req.hostname}${path}`;

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 Preserve ports when deriving OAuth callback URLs

In this Fastify v5 app, req.hostname is the host without any :port, so deployments reached at a non-default public origin such as http://localhost:8080 or https://example.com:8443 now send OAuth providers and post-login redirects to http(s)://host/api/v1/sso/... instead. Those callbacks land on the wrong port and miss the callback route/session cookie, whereas the previous SITE_URL value could include the port. Use the request host/origin value that preserves the port when building these URLs.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes OAuth login failures for users on us.infisical.com by deriving the OAuth callback URL and post-authentication redirect from req.protocol/req.hostname instead of the static SITE_URL, ensuring the session cookie domain matches the domain the user initiated the flow from.

  • Dynamic callbackURL injection: Both the redirect-initiation endpoints and the OAuth callback endpoints now call getOAuthCallbackUrl(req, path) to build a per-request callback URL passed to passport.authenticate() for all three providers (Google, GitHub, GitLab).
  • Post-auth redirect base changed: handleOAuthCallbackRedirect now uses reqOrigin (built from req.hostname) as the base URL for redirects to /login/select-organization and /signup/sso, with appCfg.SITE_URL as a nominal fallback that is never actually reached because getOAuthCallbackUrl always returns a non-empty string.

Confidence Score: 3/5

The change solves the session-cookie domain mismatch but introduces an unvalidated host-header value into both OAuth callback URL construction and post-auth redirect targets; needs a trusted-host allowlist before merging.

The core fix is correct conceptually, but req.hostname (HTTP Host header) and req.protocol (X-Forwarded-Proto, trusted by default) are both attacker-controllable. Using them to build post-auth redirect URLs without an allowlist check creates an open redirect on the OAuth callback path. Separately, the SITE_URL fallback at lines 506/517 is dead code — getOAuthCallbackUrl always returns a non-empty string, so a missing or blank Host header causes a TypeError instead of a graceful fallback.

backend/src/server/routes/v1/sso-router.ts — specifically the getOAuthCallbackUrl helper and its use in handleOAuthCallbackRedirect

Security Review

  • Open redirect via Host header injection (sso-router.ts, handleOAuthCallbackRedirect): getOAuthCallbackUrl uses req.hostname (HTTP Host header) and req.protocol (X-Forwarded-Proto when trustProxy: true) without any validation against a trusted-host allowlist. On the OAuth callback endpoints, this value is used directly as the base URL for post-authentication redirects. An attacker who can set the Host header on a callback request (e.g. by hitting the backend directly, bypassing the reverse proxy) can redirect an authenticated user to an arbitrary domain.
  • Attacker influence over OAuth callbackURL: The same unvalidated req.hostname is passed as callbackURL to passport.authenticate() on the redirect-initiation endpoints. While OAuth providers reject unregistered callback URLs, injecting a manipulated hostname is still a host-header attack surface that bypasses the static SITE_URL guard.

Important Files Changed

Filename Overview
backend/src/server/routes/v1/sso-router.ts Derives OAuth callbackURL and post-auth redirect base from req.hostname/req.protocol (attacker-controllable headers) without allowlist validation, creating an open redirect and a broken fallback to SITE_URL.

Reviews (1): Last reviewed commit: "fix(auth): derive OAuth callback URL fro..." | Re-trigger Greptile

Comment on lines +46 to +47
const getOAuthCallbackUrl = (req: { protocol: string; hostname: string }, path: string) =>
`${req.protocol}://${req.hostname}${path}`;

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.

P1 security Host-header injection enables open redirect post-authentication

req.hostname is derived from the HTTP Host header (and req.protocol from X-Forwarded-Proto when trustProxy: true, which is the default in app.ts). Both values are attacker-controllable without MitM. On the callback endpoints (/google, /github, /gitlab), reqOrigin is built from these headers and used as the base URL for post-auth redirects (/login/select-organization, /signup/sso). An attacker who sends the OAuth callback to the backend directly (e.g., bypassing the reverse proxy) with Host: evil.com will get the authenticated browser redirected to evil.com/login/select-organization, which is a post-login open redirect. The fix should validate req.hostname against a set of trusted origins (e.g. parsed from appCfg.SITE_URL plus any additional known hostnames) before using it in URLs, and fall back to appCfg.SITE_URL on mismatch.

Comment on lines 506 to +517
@@ -506,7 +514,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {

if (passportResult.result === ProviderAuthResult.SIGNUP_REQUIRED) {
const serverCfg = await getServerCfg();
const signupUrl = new URL("/signup/sso", appCfg.SITE_URL);
const signupUrl = new URL("/signup/sso", reqOrigin || appCfg.SITE_URL);

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.

P1 Fallback to appCfg.SITE_URL is unreachable dead code

getOAuthCallbackUrl(req, "") always returns a non-empty string — at minimum "https://" if req.hostname is empty. Because the result is always truthy, reqOrigin || appCfg.SITE_URL always resolves to reqOrigin and never reaches appCfg.SITE_URL. When req.hostname is absent or blank, new URL("/login/select-organization", "https://") throws a TypeError: Invalid URL (same applies to the /signup/sso redirect), causing an unhandled 500 rather than a graceful fallback. The intended guard should check that req.hostname is non-empty before building reqOrigin, or the fallback condition should compare the built URL against the empty-hostname case.

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.

0 participants