Files
simbarag/blueprints/users/oidc_state.py
T
ryan bc3e3d2797 Accept access tokens on protected routes; persist OIDC state
- 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>
2026-10-12 17:44:34 -04:00

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