Skip to content

feat(auth): add GET method to /auth/sign-out/ for portal logout chain#35

Open
awais786 wants to merge 4 commits into
foss-mainfrom
feat/sign-out-get-method
Open

feat(auth): add GET method to /auth/sign-out/ for portal logout chain#35
awais786 wants to merge 4 commits into
foss-mainfrom
feat/sign-out-get-method

Conversation

@awais786
Copy link
Copy Markdown

Summary

Adds a GET method to the existing `SignOutAuthEndpoint`. POST behaviour unchanged. GET reuses the same `django.contrib.auth.logout()` call and 302s to a `?next=` URL (validated against `PLATFORM_DOMAIN`).

Lets the foss-server-bundle portal's "Log out of all apps" button include Plane in a cross-origin redirect chain that walks each app's domain and ends Plane's session along the way.

Why GET (not reuse POST)

Cross-origin redirect chains follow `Location:` headers, which are GET semantics. Cross-origin POST without CORS handshake fails; cross-origin CSRF tokens aren't shareable. GET on the same endpoint is the only chain-friendly shape, hence the addition.

Diff

File Change
`signout.py` Add GET method (calls `logout()` + `?next=` validation). Class-level `@csrf_exempt`. POST untouched.
`settings/common.py` Read `PLATFORM_DOMAIN` from env.
`.env.example` Document `PLATFORM_DOMAIN`.
`tests/unit/views/test_signout.py` +6 GET-side cases. POST tests untouched.

Net: +153 / -9 across 4 files.

Tests

  • Redirect to allowlisted `?next=` (session still flushed)
  • Reject `?next=` on disallowed host (400, session still flushed)
  • Dot-boundary enforcement (`foss.arbisoft.com.evil` → 400)
  • Empty `PLATFORM_DOMAIN` rejects every `?next=`
  • `?next=` omitted → fallback to `MPASS_SIGNOUT_URL`
  • Malformed `?next=` rejected

Out of scope

  • Portal-side "Log out of all apps" button — separate PR in foss-server-bundle.
  • Analogous GET methods on Outline / Penpot / Twenty / SurfSense — separate PRs.

Comment thread apps/api/plane/authentication/views/app/signout.py Fixed
@awais786 awais786 force-pushed the feat/sign-out-get-method branch from dd79e1d to 7c90bf8 Compare May 18, 2026 13:00
Lets the foss-server-bundle portal's "Log out of all apps" button
include Plane in its cross-app redirect chain. POST behaviour is
unchanged. GET clears the Django session via the same
django.contrib.auth.logout() call and 302s to ?next= (validated
against PLATFORM_DOMAIN to prevent open redirect; suffix match
enforces dot boundary).

CSRF-exempt at the class level: no token is shared cross-origin with
the portal. The residual force-logout risk (image-tag triggers
logout) is low — only the session itself is lost; re-auth via
ForwardAuth is automatic.
@awais786 awais786 force-pushed the feat/sign-out-get-method branch from 7c90bf8 to afddc4c Compare May 18, 2026 13:03
Comment thread apps/api/plane/authentication/views/app/signout.py Fixed
@jawad-khan
Copy link
Copy Markdown

@copilot Review thoroughly, specially from security angle.

Copilot AI and others added 3 commits May 22, 2026 07:30
… CodeQL open-redirect alert

- Replace _is_allowed_next (boolean) with _sanitize_next (returns sanitized URL or None)
- Reconstruct URL from urlparse components instead of passing raw user input to HttpResponseRedirect
- Add scheme allowlist (http/https only) to block javascript: and data: URIs
- Add test case for non-http scheme rejection

Agent-Logs-Url: https://github.com/Pressingly/plane/sessions/0c64d2ca-8166-43df-9115-a9c2135346ab

Co-authored-by: jawad-khan <5320368+jawad-khan@users.noreply.github.com>
…nized sanitizer

Adds Django's built-in url_has_allowed_host_and_scheme check which CodeQL
recognizes as a proper open-redirect sanitizer. Combined with domain
validation and URL reconstruction from parsed components.

Agent-Logs-Url: https://github.com/Pressingly/plane/sessions/0c64d2ca-8166-43df-9115-a9c2135346ab

Co-authored-by: jawad-khan <5320368+jawad-khan@users.noreply.github.com>
… as redirect

Move the Django sanitizer check directly into the get() method scope so
CodeQL recognizes the guard pattern. Simplify to a single helper that
returns the validated host set for url_has_allowed_host_and_scheme.

Agent-Logs-Url: https://github.com/Pressingly/plane/sessions/0c64d2ca-8166-43df-9115-a9c2135346ab

Co-authored-by: jawad-khan <5320368+jawad-khan@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI commented May 22, 2026

@copilot Review thoroughly, specially from security angle.

Security review completed and CodeQL alert resolved in d9354fc.

Changes made:

  • Replaced custom URL reconstruction with Django's url_has_allowed_host_and_scheme as an inline guard (CodeQL-recognized sanitizer pattern)
  • Dot-boundary domain validation prevents suffix attacks (foss.arbisoft.com.evil → rejected)
  • Django's validator rejects non-http(s) schemes (javascript:, data:, etc.)
  • Added test case for non-http scheme rejection

Security layers (defense in depth):

  1. _get_allowed_hosts_for_url() — validates hostname is platform domain or proper subdomain with dot-boundary enforcement
  2. url_has_allowed_host_and_scheme() — Django's built-in open-redirect guard, validates scheme (http/https only) and host
  3. Empty PLATFORM_DOMAIN → rejects all ?next= redirects

CodeQL now reports 0 alerts for URL redirection.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a GET handler to the existing /auth/sign-out/ endpoint to support cross-origin logout redirect chains (portal “log out of all apps”), validating ?next= redirects against an allowlisted PLATFORM_DOMAIN.

Changes:

  • Implement GET /auth/sign-out/ by reusing the existing logout flow and conditionally redirecting to a validated ?next= URL.
  • Introduce PLATFORM_DOMAIN configuration (settings + .env.example) to control the redirect allowlist.
  • Add unit tests covering GET redirect allowlisting and rejection cases.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
apps/api/plane/authentication/views/app/signout.py Adds GET support, ?next= validation against PLATFORM_DOMAIN, and CSRF handling changes.
apps/api/plane/settings/common.py Adds PLATFORM_DOMAIN env-backed setting.
apps/api/.env.example Documents PLATFORM_DOMAIN (and adjusts MPASS-related comments).
apps/api/plane/tests/unit/views/test_signout.py Adds GET-focused unit tests for redirect validation behavior.

Comment on lines +20 to 22
@method_decorator(csrf_exempt, name="dispatch")
class SignOutAuthEndpoint(View):
def post(self, request):
Comment on lines +38 to +46
def get(self, request):
# Delegate to POST so last_logout_ip/_time get tracked and the
# session is flushed the same way. Only override the redirect
# target if ?next= was passed (portal logout-chain hop).
response = self.post(request)

next_url = (request.GET.get("next") or "").strip()
if not next_url:
return response
Comment on lines +149 to +155
def test_redirects_to_allowlisted_next(
self, mock_settings, mock_user_cls, mock_logout, mock_base_host, factory, view
):
mock_settings.PLATFORM_DOMAIN = "foss.arbisoft.com"
mock_user_cls.objects.get.return_value = MagicMock()
next_url = "https://docs.foss.arbisoft.com/auth/sign-out/"

Comment on lines +149 to +161
def test_redirects_to_allowlisted_next(
self, mock_settings, mock_user_cls, mock_logout, mock_base_host, factory, view
):
mock_settings.PLATFORM_DOMAIN = "foss.arbisoft.com"
mock_user_cls.objects.get.return_value = MagicMock()
next_url = "https://docs.foss.arbisoft.com/auth/sign-out/"

response = view.get(_make_get_request(factory, f"next={next_url}"))

mock_logout.assert_called_once() # GET still flushes the session
assert response.status_code == 302
assert response["Location"] == next_url

Comment thread apps/api/.env.example
Comment on lines 77 to +81
# mPass proxy auth
# Optional: comma-separated paths to skip proxy auth (default: /god-mode,/api/instances)
# MPASS_BYPASS_PATHS=/god-mode,/api/instances
# Required for 3-layer logout (Django → oauth2-proxy → Cognito)
# MPASS_SIGNOUT_URL=https://foss-auth.local.moneta.dev/oauth2/sign_out?rd=https%3A%2F%2Fcognito.example.com%2Flogout
# PLATFORM_DOMAIN=foss.arbisoft.com
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.

5 participants