🐛 Re-key session when X-Auth-Request identity differs#22
Conversation
8754bfd to
4592133
Compare
There was a problem hiding this comment.
Pull request overview
This PR updates backend X-Auth-Request session handling so proxy-authenticated identity can replace a stale local session when the resolved profile differs.
Changes:
- Reworks
wrap-authzto resolve proxy identity even when a session exists, re-keying on mismatch. - Adds a session renewal guard to avoid overwriting an auth cookie already set by downstream middleware.
- Adds middleware tests for re-keying, steady-state behavior, stale auth-data cleanup, and renewal interaction.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
backend/src/app/http/auth_request.clj |
Adds mismatch detection and re-key behavior for X-Auth-Request sessions. |
backend/src/app/http/session.clj |
Skips session renewal when the response already includes the auth-token cookie. |
backend/test/backend_tests/http_middleware_test.clj |
Adds and updates tests covering X-Auth-Request re-key scenarios. |
Comments suppressed due to low confidence (5)
backend/src/app/http/auth_request.clj:219
- This leaves
::session/sessionpointing at the previously decoded session while the request's::session/profile-idhas been changed to the proxy user. That stale session is observable:session/get-sessionis used byupdate-profile-passwordto callinvalidate-others, so a password-change RPC on the re-keying request would invalidate sessions for the old user instead of the newly authenticated one.
;; ::session/session is intentionally
;; NOT dissoc'd here — the surrounding
;; session.clj wrap-authz closes over
;; the originally-decoded session for
;; renewal, and no external code reads
;; ::session/session post-handler.
(dissoc ::http/auth-data)
backend/src/app/http/auth_request.clj:223
- This unconditionally creates a new auth-token after the downstream handler has already built its response. If the re-keying request is an RPC that intentionally clears or replaces the auth-token (for example logout or delete-profile via their response transforms), this outer create-session call overwrites that cookie with a fresh proxy session, making the logout/delete response leave the browser authenticated again.
;; Fresh auth-token cookie; replaces the stale one the
;; browser still has (if any).
(create-session! request response)))))))))
backend/src/app/http/auth_request.clj:219
- The PR description lists
::http/auth-datacleanup as intentionally dropped from this minimal change, but this branch now adds that cleanup and a dedicated test for it. Please either update the description/test plan or remove the cleanup so reviewers are evaluating the actual scope.
;; Drop ::http/auth-data so downstream
;; readers don't see the previous user's
;; claims after we re-key. Confirmed
;; consumers: errors.clj logs
;; auth-data.claims.uid as
;; :request/profile-id; rpc/helpers
;; exposes the map to RPC handlers via
;; get-auth-data.
;;
;; ::session/session is intentionally
;; NOT dissoc'd here — the surrounding
;; session.clj wrap-authz closes over
;; the originally-decoded session for
;; renewal, and no external code reads
;; ::session/session post-handler.
(dissoc ::http/auth-data)
backend/src/app/http/auth_request.clj:161
- This log message is no longer accurate when
session-pidis present: the branch preserves the existing session and calls the handler authenticated as that user, not unauthenticated. This will make mismatch/unresolvable-header incidents harder to diagnose from logs.
(l/wrn :hint "x-auth-request: no profile found for email, passing through unauthenticated"
:email email)
(handler request))
backend/src/app/http/auth_request.clj:161
- When a proxy-authenticated email cannot be resolved, this preserves any existing local session and continues the request as that previous user. In the same stale-cookie flow, logging in upstream as an unprovisioned user (or during an auto-register/config issue) would still serve the old Penpot account instead of clearing/denying the stale session.
(nil? profile)
;; Header email doesn't resolve to a profile (and auto-register
;; is off). No identity to switch *to* — pass through with
;; whatever wrap-session set.
(do
(l/wrn :hint "x-auth-request: no profile found for email, passing through unauthenticated"
:email email)
(handler request))
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ;; the re-key is silently undone. | ||
| (if (and session | ||
| (renew-session? session) |
| profile (try | ||
| (get-or-register-profile cfg email fullname) |
4592133 to
016c30b
Compare
|
Thanks @copilot — three valid findings. Addressed in 1. Real bug; my earlier "no external readers" claim was wrong — I grepped for the literal keyword but missed the indirect read through 2. Renewal extends alice's cookie on a 403 mismatch denial 🔴 Real bug. When proxy-asserted profile is blocked/inactive, 3. Performance regression — every steady-state SSO request hits the DB 🟡 Real cost; intentionally documented rather than fixed. Removing the "skip if session already resolved" fast path is what enables mismatch detection — without resolving the header email's profile, we can't know if the existing session still matches. Trading throughput for correctness on the steady-state path. Inline PR head: |
Replaces #21 with a minimal scope. Closes the QA-reported session-sharing repro and nothing else.
130 insertions / 53 deletions across 3 files (vs 540/70 on #21).
The bug
wrap-authzskipped whenwrap-sessionhad already set::session/profile-idfrom theauth-tokencookie. Once Penpot issued its own cookie, the upstreamX-Auth-Request-Emailwas no longer consulted — so after a portal "Log out of all apps" + login as a different user, refreshing the Penpot tab kept serving the previous user. The shared_oauth2_proxycookie + Cognito session were cleared, but Penpot's ownauth-tokenonpaint.<domain>survived.The fix
Detect mismatch and re-key.
auth-tokencookie viasession/create-fn.session.cljguard: don't renew if the response already has theauth-tokencookie. Without this, the renewal path (which runs afterwrap-authzon the way out) would clobber the freshly-issued cookie with a renewed copy of the previous user's.Blocked / inactive / nil-profile / API-key (
::actoken/profile-id) branches are unchanged.Repro
paint.<domain>) in a tab.What's NOT in this PR (dropped from #21 — can land separately)
These tighten edge cases but are not needed to fix the QA repro:
session/delete-fn::http/auth-datacleanup channelsession.cljwith-session-id+clear-stale-session+response-has-auth-cookie?helpersIf/when any of those become load-bearing, open as follow-ups against the same capability.
Test plan
clj -X:test :nses '[backend-tests.http-middleware-test]'passes — three new/renamed cases:x-auth-request-rekeys-when-session-identity-differs— alice session-pid + bob email → captured request has bob's pid, response has freshauth-tokencookiex-auth-request-no-rekey-when-session-matches-header— steady-state guard (no new cookie when match)x-auth-request-preserves-session-when-header-email-unresolvable— renamed from the priorskips-when-session-present; invariant is now "preserve existing session when there's no real alternative identity to switch to"x-auth-request-no-email-header,-skips-when-access-token-present,-authenticates-existing-active-profile,-blocked-profile-returns-403,-inactive-profile-returns-403,-unknown-email-no-autoregister,-auto-register-creates-active-profile.Action requested
Either:
git push origin +shrink/proxy-auth-stale-session-minimal:fix/proxy-auth-stale-session-on-user-switch) and close this PR.Same outcome either way — pick whichever preserves the review history you'd rather keep.
🤖 Generated with Claude Code