Skip to content

fix(auth): deep links lose path and query through the oauth2-proxy login redirect #3169

Closed

Description

@devantler

🤖 Generated by the Agentic Engineer

Evidence

All seven oauth2-proxy-gated HTTPRoutes set a fixed, per-host root value for
X-Auth-Request-Redirect:

Route Value
apps/homepage https://${domain}/
apps/backstage https://backstage.${domain}/
infrastructure/opencost https://opencost.${domain}/
infrastructure/policy-reporter https://policy-reporter.${domain}/
controllers/cilium (hubble) https://hubble.${domain}/
controllers/coroot https://observability.${domain}/
controllers/ksail-operator https://ksail.${domain}/

oauth2-proxy runs with reverse_proxy = true
(controllers/oauth2-proxy/helm-release.yaml:143), and in that mode it gives
X-Auth-Request-Redirect priority over the forwarded-URI headers.

Audience and problem

Anyone following a deep link into one of these UIs while logged out. After
authenticating they land on the service root, not the page they asked for —
the original path and query string are discarded. It affects shared links to a
specific Coroot dashboard, an OpenCost report, a Backstage entity page, or a
KSail cluster view, and any bookmarked deep link after the session cookie expires.

Expected behaviour

After the OIDC round-trip the user arrives at the URI they originally requested,
path and query preserved.

Why this is not a per-route fix

The value is identical in shape across all seven routes, so correcting one leaves
the other six broken and makes that route inconsistent. Gateway API's
RequestHeaderModifier cannot interpolate a dynamic per-request value, so the fix
is necessarily shared: either drop the fixed header and let oauth2-proxy derive the
target from trusted forwarded headers (X-Forwarded-Uri), or supply a dynamic
redirect another way — both of which change shared oauth2-proxy configuration and
need validating against every consumer at once.

Acceptance criteria

  • A logged-out request to a deep link (e.g. https://observability.${domain}/<some-view>?<query>) lands back on that exact URI after authenticating.
  • Verified on at least two of the seven services, one infrastructure and one app.
  • The remaining five behave the same way (no route left on the old fixed-root pattern).
  • No regression in the login flow itself, including the CSRF/cookie behaviour documented in the oauth2-proxy HelmRelease comments.

Size

Small-to-medium: the change is concentrated in oauth2-proxy config plus removing
the header from seven routes, but it needs a real end-to-end auth check rather
than static validation.

Notes

Surfaced by a CodeRabbit finding on platform#2723, where the ksail-operator route
adopted the existing pattern. Refuted as out of scope there — the route follows the
established convention, and fixing it alone would not fix the defect.

Related to #1881, which would replace the auth-proxy hop with a Gateway API
ExternalAuth filter and revisits these headers as part of that migration; this
issue is the current-architecture defect and should not wait on it.

Activity

  1. added theissue type on Aug 16, 2026
  2. moved this to 📥 Backlog in 🌊 Project Boardon Aug 16, 2026
  3. coderabbitai commented on Aug 16, 2026

    @coderabbitai
    Contributor
    🔗 Related PRs

    #2728 - fix(apps): restrict public Umami routes [merged]
    #2740 - fix(security): restrict tenant gateway hostnames [merged]
    #2742 - fix(apps): gate Actual Budget behind oauth2-proxy [merged]
    #3109 - fix(deploy): avoid redundant GHCR node reboots [merged]


    📝 Issue Planner

    Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    🧪 Issue enrichment is currently in open beta.

    You can configure auto-planning by selecting labels in the issue_enrichment configuration.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  4. self-assigned this
    on Sep 25, 2026
  5. devantler commented on Sep 25, 2026

    @devantler
    ContributorAuthor

    🤖 Generated by the Agentic Engineer

    Picked up — the scope is wider than the issue records, and the fix is a staged change to where logins finish

    Measured live on prod (2026-09-25 ~10:50Z), following a logged-out deep link on four hosts and reading the target oauth2-proxy stores in the login state:

    Host Stored post-login target Result after login
    observability. https://observability.<domain>/ lands on the root — path and query lost
    backstage. https://backstage.<domain>/ lands on the root — path and query lost
    analytics. /websites?view=1 (relative) lands on the oauth2-proxy host, which serves no app
    budget. /budget?m=2 (relative) lands on the oauth2-proxy host, which serves no app

    So there are two defects with one root, and the second is worse than the one this issue describes: the routes that dropped the fixed header (#2742, #3813) keep the path, but every login finishes at the single oauth2-proxy.<domain> callback, so a relative target resolves against the wrong host.

    Root cause. oauth2-proxy (v7.15.3, read from its source) completes a login at redirect_url, and ours is fixed to one central host. A fixed per-route root header hides that for eight routes; the two without it show it.

    Fix. Let oauth2-proxy finish each login on the host it started on (drop the fixed redirect_url; it then derives https://<host>/oauth2/callback, forced to https by cookie_secure), after which the relative target is correct on every host and the per-route headers can go. It ships in three steps so no login is ever pointed at a callback Dex does not yet accept:

    1. Register https://<host>/oauth2/callback on Dex's public-client for every fronted host, with a guard that derives the hosts from the rendered overlays — no behaviour change.
    2. Switch oauth2-proxy to per-host callbacks — fixes analytics and budget outright.
    3. Remove the fixed X-Auth-Request-Redirect header from the eight routes — deep links survive login everywhere.

    The open-redirect surface is unchanged: rd already takes precedence over the header, and targets are still validated against the same whitelist_domains.

  6. devantler commented on Sep 25, 2026

    @devantler
    ContributorAuthor

    🤖 Generated by the Agentic Engineer

    Verified live on 2026-09-25 at 19:3xZ. The fixes (#4181, #4185, #4191) merged earlier today, but the gateway was not applying route changes until #4199 restarted the Cilium operator (#4198). So the header change in #4191 only reached users at 19:33Z.

    Checked read-only: an unauthenticated request for /deep/link/check?from=now-1h on all 10 SSO-protected hosts now redirects to Dex with that host's own /oauth2/callback and returns to the exact path and query after login. Those hosts are platform, analytics, backstage, budget, crossview, hubble, ksail, observability, vault and policy-reporter.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Fields

Priority

None yet

Effort

None yet

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions