Marker-cookie pattern per docs/conventions/per-app-edge-identity-validation.md: - New BbEdgeIdentityMiddleware compares `_bb_edge_sub` cookie to `X-Auth-Request-User` header on every authenticated request. On mismatch, flushes the Django session and replaces request.user with AnonymousUser so DRF returns 401 / browser navigations land at the bridge handoff redirect. Lazy-populates the cookie on legacy sessions; passes through for anonymous requests and bearer-token-only callers. - Trusted-JWT view sets `_bb_edge_sub` on the redirect response when X-Auth-Request-User is present (single session-mint choke-point — the Bucket-4 entry-point is the only path that creates Plane sessions in this deployment). - SignOutAuthEndpoint reads optional BB_LOGOUT_REDIRECT_URL env. When set, the SPA's /auth/sign-out/ form-POST is 302'd to the platform bridge's synced-logout endpoint (clears edge `_bb_oauth2` + back-channels Zitadel end_session). Without this, the user's Zitadel session at the edge outlives the Plane logout and silently re-logs them in via bridge handoff → trusted sign-in. Vanilla regression-safe: env unset → upstream behavior. Net surface vs upstream-clean: 1 new middleware file, 1 line in MIDDLEWARE, ~20 lines added to trusted.py and signout.py. No new dependencies. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
55 lines
2.2 KiB
Python
55 lines
2.2 KiB
Python
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# See the LICENSE file for details.
|
|
|
|
# Python imports
|
|
import os
|
|
|
|
# Django imports
|
|
from django.views import View
|
|
from django.contrib.auth import logout
|
|
from django.http import HttpResponseRedirect
|
|
from django.utils import timezone
|
|
|
|
# Module imports
|
|
from plane.authentication.utils.host import user_ip, base_host
|
|
from plane.db.models import User
|
|
|
|
|
|
# binarybeachio fork addition. When set, the SPA's /auth/sign-out/ form-POST
|
|
# (apps/web/core/services/auth.service.ts) gets 302'd here instead of back to
|
|
# the app root. The platform bridge's /logout endpoint clears the
|
|
# oauth2-proxy `_bb_oauth2` cookie AND back-channels Zitadel's end_session,
|
|
# so signing out from inside Plane now propagates to the edge — without it,
|
|
# the user lands back on / with the Zitadel session still alive at the edge,
|
|
# auto-redirects through plane-signin-redirect → bridge handoff → trusted
|
|
# sign-in, and is silently re-logged-in as the same identity.
|
|
#
|
|
# Read at request time so dashboard env-var changes don't require a rebuild.
|
|
# See binarybeachio/docs/services/auth-bridge/session-debrief-2026-05-04-edge-validation-and-logout.md
|
|
# §B "Bundle with each rollout".
|
|
_BB_LOGOUT_REDIRECT_URL_ENV = "BB_LOGOUT_REDIRECT_URL"
|
|
|
|
|
|
class SignOutAuthEndpoint(View):
|
|
def post(self, request):
|
|
# Get user
|
|
try:
|
|
user = User.objects.get(pk=request.user.id)
|
|
user.last_logout_ip = user_ip(request=request)
|
|
user.last_logout_time = timezone.now()
|
|
user.save()
|
|
# Log the user out
|
|
logout(request)
|
|
except Exception:
|
|
pass
|
|
|
|
bb_logout_url = os.environ.get(_BB_LOGOUT_REDIRECT_URL_ENV) or ""
|
|
target = bb_logout_url or base_host(request=request, is_app=True)
|
|
response = HttpResponseRedirect(target)
|
|
# Clear the edge-identity marker cookie alongside the session. The
|
|
# SessionMiddleware will delete the session-id cookie on its own once
|
|
# request.session.is_empty() (Django logout() flushes it), but the
|
|
# marker has no equivalent owner — clear it explicitly here.
|
|
response.delete_cookie("_bb_edge_sub", path="/")
|
|
return response
|