feat(auth): add GET /auth/portal-sign-out/ for cross-app logout chain#31
Closed
awais786 wants to merge 1 commit into
Closed
feat(auth): add GET /auth/portal-sign-out/ for cross-app logout chain#31awais786 wants to merge 1 commit into
awais786 wants to merge 1 commit into
Conversation
The foss-server-bundle portal's "Log out of all apps" button needs to
clear each app's session cookie in addition to the shared _oauth2_proxy
cookie and the Cognito SSO session. Cross-origin Set-Cookie isn't a
thing — only the app whose origin owns the cookie can clear it — so
the portal has to navigate the browser through each app's domain.
This endpoint is the Plane participant in that chain:
GET /auth/portal-sign-out/?next=<absolute_url>
1. logout(request) — flushes the Django session
2. Validates next against MPASS_SIGNOUT_NEXT_ALLOWED_HOSTS (suffix
match on a dot boundary; ".foss.arbisoft.com" matches subdomains
but not "foss.arbisoft.com.evil")
3. 302 to next
CSRF-exempt because the portal cannot obtain Plane's CSRF token cross-
origin. The residual risk is force-logout (an attacker embeds
<img src="…/portal-sign-out"> and ends the victim's session). That's
low-impact: the only state lost is the session itself, and re-auth via
ForwardAuth is automatic on the next request.
When ?next= is omitted or invalid, falls back to MPASS_SIGNOUT_URL
(if set) so a direct hit to the endpoint still chains through
oauth2-proxy + Cognito.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| return HttpResponseBadRequest( | ||
| "next= target host is not in MPASS_SIGNOUT_NEXT_ALLOWED_HOSTS" | ||
| ) | ||
| return HttpResponseRedirect(next_url) |
3 tasks
Author
|
Closing — the portal-driven logout chain (where the portal redirects through each app's logout URL) was an alternative to middleware self-healing. Going with #29's middleware mismatch detection as the load-bearing fix; portal can keep doing what it already does (clear _oauth2_proxy + Cognito) and Plane self-corrects on the next request. Can revisit if synchronous cleanup becomes a hard requirement. |
awais786
added a commit
to Pressingly/outline
that referenced
this pull request
May 18, 2026
Lets the foss-server-bundle portal's "Log out of all apps" button
include Outline in the redirect chain. Browser security forbids
cross-origin Set-Cookie, so the portal cannot clear Outline's
accessToken cookie on docs.<domain> directly — it has to navigate the
browser to a same-origin Outline URL that clears the cookie itself.
GET /auth/portal-logout?next=<absolute_url>
1. Clears accessToken + lastSignedIn cookies (same shape as the
auth() catch block already uses).
2. Validates next against MPASS_SIGNOUT_NEXT_ALLOWED_HOSTS — suffix
match on a dot boundary, so foss.arbisoft.com matches its
subdomains but NOT foss.arbisoft.com.evil.example.
3. 302s to next, or returns 200 if next is missing/invalid (cookies
still cleared).
CSRF-exempt because the portal cannot share Outline's CSRF token
cross-origin. Residual risk is force-logout (img-tag embedding) — low
impact: ForwardAuth re-auths on the next request.
Empty allowlist rejects every ?next= — endpoint still clears cookies,
just won't redirect. Non-SSO deployments unaffected by default.
Mirrors Pressingly/plane#31 for the same flow on Plane's side.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lets the foss-server-bundle portal's "Log out of all apps" button include Plane in the redirect chain. Existing
POST /auth/sign-out/is CSRF-protected, so the portal can't redirect into it cross-origin. This adds aGETvariant that the portal can navigate the browser through.The portal builds a nested chain like:
Each app clears its own session cookie while the browser is on that app's domain (so
Set-Cookiescope is correct), and the final hop lands at the portal.Endpoint
GET /auth/portal-sign-out/?next=<absolute_url>logout(request)— flushes the Django sessionnextagainst the env-configured allowlistnext(or toMPASS_SIGNOUT_URLifnextis omitted)CSRF-exempt — the portal can't share a CSRF token cross-origin. Residual risk is force-logout (an attacker embeds
<img src=".../portal-sign-out">and the victim's session ends). Low impact: the only state lost is the session itself, and ForwardAuth re-auths on the next request.New setting
Suffix match enforces a dot boundary, so
foss.arbisoft.commatchespm.foss.arbisoft.combut notfoss.arbisoft.com.evil.example— closes the obvious open-redirect surface.Test plan
pytest apps/api/plane/tests/unit/views/test_portal_signout.py -vpasses — covers:next=on allowlisted host → 302 +logout()callednext=→ falls back toMPASS_SIGNOUT_URLnext=and noMPASS_SIGNOUT_URL→ 302 to/next=on disallowed host → 400foss.arbisoft.com.evilrejected)docs.foss.arbisoft.com) matches the suffix entry?next=?next=(no hostname) → 400.foss.arbisoft.com(with leading dot) is normalisedGET /auth/portal-sign-out/?next=https://docs.foss.arbisoft.com/auth/portal-logoutwith a Django session cookie — verify session cookie is cleared in the response andLocationheader is thenextURL.next=https://evil.example/x— verify 400.Relation to other PRs
ProxyAuthMiddleware). Recovery path: catches stale sessions on the next request when upstream identity differs. Already triggers automatically after the portal flow; this PR makes the cleanup synchronous instead of lazy.Open follow-up
Outline / Penpot / Twenty will each need an analogous endpoint in their own session technology to fully participate in the portal redirect chain. Those PRs are separate.
🤖 Generated with Claude Code