bb-plane-fork/apps/api/plane/authentication/views/app/signout.py
binarybeach 64513797ee binarybeachio: per-app edge-identity validation + bundled bridge logout
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>
2026-05-05 13:31:02 -10:00

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