bc3e3d2797
- Add transitional @jwt_access_required (accepts access tokens, and for now also refresh tokens) and use it on every protected route and inside admin_required. /api/user/refresh stays refresh-only. - Move pending OIDC state/PKCE verifiers from an in-process dict to an oidc_states table (10 min TTL, single-use) so logins survive a blue/green flip. Drop the unused redirect_after_login. - Unit tests for the decorator and the state store. Deploy this commit on its own, before the access-token frontend: the old backend answers access tokens with 422, so the new frontend only works once this backend is live on both colors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
37 lines
1.2 KiB
Python
37 lines
1.2 KiB
Python
"""
|
|
Persistent, single-use store for pending OIDC logins (state -> PKCE verifier).
|
|
|
|
Lives in Postgres so a login started on one blue/green color can finish on
|
|
the other.
|
|
"""
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from .models import OIDCState
|
|
|
|
OIDC_STATE_TTL = timedelta(minutes=10)
|
|
|
|
|
|
async def save_oidc_state(
|
|
state: str, pkce_verifier: str, ttl: timedelta = OIDC_STATE_TTL
|
|
) -> None:
|
|
now = datetime.now(timezone.utc)
|
|
# Opportunistic cleanup of abandoned logins
|
|
await OIDCState.filter(expires_at__lte=now).delete()
|
|
await OIDCState.create(
|
|
state=state, pkce_verifier=pkce_verifier, expires_at=now + ttl
|
|
)
|
|
|
|
|
|
async def consume_oidc_state(state: str) -> str | None:
|
|
"""Return the PKCE verifier for `state` and delete it, or None if the
|
|
state is unknown, expired, or was already consumed."""
|
|
row = await OIDCState.get_or_none(state=state)
|
|
if row is None:
|
|
return None
|
|
# The delete is the single-use guard: only one concurrent caller wins.
|
|
deleted = await OIDCState.filter(state=state).delete()
|
|
if not deleted or row.expires_at <= datetime.now(timezone.utc):
|
|
return None
|
|
return row.pkce_verifier
|