Geo: Derive OIDC redirect_uri from proxying secondary site

What does this MR do and why?

Geo: Derive OIDC redirect_uri from proxying secondary site

What does this MR do and why?

With Geo separate URLs + secondary proxying, OIDC sign-in can only ever complete on one site's domain. omniauth_openid_connect sends the static client_options.redirect_uri for every login regardless of which site the user signed in on (gem source), so the callback lands on the wrong domain, where the OmniAuth state isn't in the session, failing with Invalid 'state' parameter. This has been a documented limitation since 2023 and affects six customers on the issue.

The EE mechanism for per-site hosts already exists: on a proxied request, OmniAuth's full_host resolves to the proxying secondary's URL (ee/lib/ee/gitlab/omniauth_initializer.rb, via Gitlab::Geo.proxied_site(env)) — the OIDC gem just never consults it. SAML already derives its callback per-request via full_host, which is why our docs' "don't configure an ACS URL" guidance works per-site for SAML (see #491634 (closed)).

The fix: a module prepended to OmniAuth::Strategies::OpenIDConnect that derives redirect_uri as full_host + callback_path when the request carries a valid Geo proxy header, aligning OIDC with SAML's existing behaviour. Requests without the header (all non-Geo instances, direct requests to the primary) hit super — unchanged. The guard requires the JWT-signed Workhorse header (ee/lib/gitlab/geo/signed_data.rb), which can't be spoofed without a secondary's signing key. Both OIDC legs (authorize + token exchange) see the same proxied request env, so both derive the same value, satisfying OIDC's redirect_uri consistency requirement.

Gated behind new ops flag geo_oidc_proxied_redirect_uri, disabled by default.

Admin prerequisite (documented): register every site's callback URL (https://<site-external-url>/users/auth/openid_connect/callback) as a valid redirect URI on the OIDC client at the IdP — standard multi-domain OIDC, one client, multiple redirect URIs.

Closes #396745.

Scope / known limitations

  • openid_connect strategy only; OAuth2-family providers with static redirect config (e.g. azure_oauth2) are follow-up material.
  • The post-login landing redirect still targets the primary's host on CNG deployments with default ingress config — that's #381573 (NGINX Ingress clobbers X-Forwarded-Host; Rails support exists since !84368 (merged)), resolved by the workaround documented there (use-forwarded-headers: true). Verified locally that this MR + that workaround = fully seamless flow.
  • OIDC logout (post_logout_redirect_uri) has the same static shape — follow-up.
  • Prepending to OpenIDConnect also covers subclasses such as CellsAwareOpenidConnect. The override is inert unless Geo is enabled, the ops flag is enabled, and the request carries a valid signed Geo proxy header - none of which hold on current Cells deployments (Geo does not run on GitLab.com today).

Testing summary

Validated on a live two-site Geo pair (Helm chart 10.1.1 / GitLab 19.1.1 CNG, separate URLs + proxying, Keycloak 26):

Test Setup Result
Repro Customer config, stock images Secondary login fails Invalid 'state' parameter; callback lands on primary domain
Config matrix Static redirect_uri pointed at each site in turn Exactly one site works per static value — no configuration fixes this
Fix These exact files mounted into stock images, flag enabled Both sites log in concurrently on their own domains; both OIDC legs consistent; clone widget shows per-site URLs
Flag off Same, flag at default (disabled) Stock behaviour, bit-for-bit
First login Brand-new IdP user via secondary User auto-created; callback executes on primary Rails (0 processed by secondary) — no replica-write issues
Regression Geo replication/verification health green; accelerated git clone via secondary; password login; non-proxied OIDC unchanged

How to set up and validate locally

Needs a two-site Geo environment with separate URLs and secondary proxyingGDK Geo (two GDKs) works; no cloud-native setup required. Plus any OIDC IdP — a local Keycloak container is easiest:

  1. Run Keycloak, create a realm, a confidential client, and a test user. Register both sites' callback URLs on the client: http://<primary>/users/auth/openid_connect/callback and http://<secondary>/users/auth/openid_connect/callback.
  2. Configure the same openid_connect OmniAuth provider on both sites (client_options.redirect_uri pointing at the primary, as a real customer would have it).
  3. Without the flag: sign in via the secondary's URL → observe the redirect to the IdP carrying the primary's redirect_uri, and the failure Could not authenticate you from OpenIDConnect because "Invalid 'state' parameter".
  4. Feature.enable(:geo_oidc_proxied_redirect_uri) on the primary.
  5. Sign in via the secondary again → the IdP authorize URL now carries the secondary's redirect_uri (visible in the browser URL bar) → login completes with the session on the secondary's domain; the project Code dropdown shows the secondary's clone URLs.
  6. Sign in via the primary in parallel → unaffected.

Changelog: fixed

EE: true

References

Screenshots or screen recordings

Before After

How to set up and validate locally

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Related to #396745

Edited by Scott Murray

Merge request reports

Loading