Skip to content

fix(signout): chain oauth2-proxy and Cognito logout after Django sign-out#30

Closed
awais786 wants to merge 1 commit into
foss-mainfrom
fix/signout-clear-oauth2-proxy-and-cognito
Closed

fix(signout): chain oauth2-proxy and Cognito logout after Django sign-out#30
awais786 wants to merge 1 commit into
foss-mainfrom
fix/signout-clear-oauth2-proxy-and-cognito

Conversation

@awais786

Copy link
Copy Markdown

Summary

  • UserStore.signOut() was clearing only Layer 1 (Django session via POST /auth/sign-out/) and then redirecting to the portal. The two upstream layers were left intact:
    • Layer 2_oauth2_proxy cookie + Redis session
    • Layer 3 — Cognito SSO session
  • Result: after clicking "Sign out", the next visit to any app in the bundle silently re-authenticated as the same user — and Cognito's hosted UI didn't prompt for a password either, because its SSO session was still alive.
  • VITE_OIDC_LOGOUT_URL and VITE_OIDC_CLIENT_ID were already configured in apps/web/.env. buildOAuth2SignOutUrl() was already exported from apps/web/core/lib/oauth2-proxy.ts. The wiring was sat there unused.
  • Fix: chain all three layers.

New flow

POST /auth/sign-out/                       ← Layer 1 (Django, await response)
  ↓ resetOnSignOut() (clear mobx in-memory state)
GET /oauth2/sign_out?rd=<cognito-logout>   ← Layer 2 (oauth2-proxy cookie + Redis)
  ↓ oauth2-proxy 302
GET <cognito-domain>/logout?client_id=...
        &logout_uri=<portal-url>           ← Layer 3 (Cognito SSO session)
  ↓ Cognito 302
<portal-url>                               ← outside ForwardAuth, final landing

When VITE_OIDC_LOGOUT_URL / VITE_OIDC_CLIENT_ID are not set, falls back to /oauth2/sign_out only — best effort for non-mPass deployments.

Cognito config requirement

Per CLAUDE.md "Cognito requirement": the portal URL must be registered as an Allowed sign-out URL in the Cognito app client.

  • Dev: http://localhost
  • Prod: https://<plane-host's parent domain>

If this is missing, Cognito will reject the logout_uri and the user will see a Cognito error page. This is a config concern, not a code one.

Repro / verification

Before:

  1. Log in to Plane via Cognito.
  2. Click "Sign out" in Plane.
  3. Visit any other app in the bundle (or just refresh) → silently logged back in as the same user. Visit Cognito hosted UI → no password prompt, you're already there.

After:

  1. Log in to Plane via Cognito.
  2. Click "Sign out" in Plane.
  3. Browser walks the redirect chain: Plane → /oauth2/sign_out → Cognito /logout → portal.
  4. Visit any other app → redirects to Cognito hosted UI → password prompt (SSO session gone).

Test plan

  • Manual: log in to Plane, click sign-out, watch DevTools Network — verify the three-hop redirect chain fires and the final URL is the portal.
  • Manual: after sign-out, hit Outline / Penpot / Twenty — each should prompt at Cognito's hosted UI (not silently re-auth).
  • Manual: after sign-out, hit Plane again — Cognito should prompt rather than auto-log in.
  • Edge case: unset VITE_OIDC_LOGOUT_URL in .env, rebuild web — sign-out still clears oauth2-proxy (visible in Network tab); Cognito session unaffected (documented best-effort fallback).
  • Confirm Cognito app client's "Allowed sign-out URLs" list includes the dev/prod portal URLs.

Complementary

This is the client-side counterpart to #29 (server-side mismatch detection). Both fix the "stale session" class of bug from different angles:

  • This PR — clear the upstream layers correctly when the user explicitly logs out.
  • fix: drop stale Django session when proxy identity changes #29 — self-correct on the server when upstream identity has changed by some other path (Cognito timeout, portal "Log out of all apps", admin deactivation, sibling app sign-out).

Out of scope

Outline / Penpot / Twenty have their own in-app sign-out buttons with the same single-layer-only bug. Separate PRs forthcoming if needed — the per-app mismatch detection PRs (#19, #18, #8 respectively) already cover the recovery path in the meantime.

🤖 Generated with Claude Code

…-out

UserStore.signOut() previously cleared only Layer 1 (Django session via
POST /auth/sign-out/) before redirecting to the portal. The two
remaining auth layers — the _oauth2_proxy cookie + Redis session, and
the Cognito SSO session — were left intact. The next visit to any app
in the bundle silently re-authenticated as the same user, no Cognito
prompt either, because the upstream SSO session was still alive.

VITE_OIDC_LOGOUT_URL and VITE_OIDC_CLIENT_ID were already configured in
.env, and buildOAuth2SignOutUrl() was already exported from
lib/oauth2-proxy.ts — the wiring was sat there unused.

After this commit, the redirect chain is:

  POST /auth/sign-out/                    ← Layer 1 (Django)
  → /oauth2/sign_out?rd=<cognito-logout>  ← Layer 2 (oauth2-proxy cookie + Redis)
  → <cognito>/logout?...&logout_uri=...   ← Layer 3 (Cognito SSO session)
  → portal URL                            ← lands outside ForwardAuth

When OIDC env vars are not set, falls back to oauth2-proxy-only (best
effort) so non-mPass deployments still see Layer 2 cleared.

Cognito's allowed sign-out URLs must include the portal URL — see
CLAUDE.md "Cognito requirement".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@awais786

Copy link
Copy Markdown
Author

Closing — the middleware mismatch detection in #29 covers the session-sharing repro. The in-app signOut 3-layer chain (Django → oauth2-proxy → Cognito) is correct behavior but not load-bearing given #29 self-corrects on any path that produces an upstream-identity change. Can revisit if/when we explicitly want synchronous (vs lazy) logout.

@awais786 awais786 closed this May 16, 2026
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.

1 participant