Skip to content

feat(auth): add dormant BFF token-handler backend infra (Phase 2)#213

Merged
philmerrell merged 2 commits into
developfrom
feature/bff-phase-2-backend-infra
May 2, 2026
Merged

feat(auth): add dormant BFF token-handler backend infra (Phase 2)#213
philmerrell merged 2 commits into
developfrom
feature/bff-phase-2-backend-infra

Conversation

@philmerrell

Copy link
Copy Markdown
Contributor

Summary

  • Phase 2 of the Bearer→cookie BFF migration. Lands the server-side plumbing dormant — no behavior change until a session cookie exists, which won't happen until Phase 3 ships the /auth/* routes.
  • Adds apis/shared/sessions_bff/ package: BFFConfig (env-driven, gated by is_enabled()), SessionRepository (DDB CRUD with TTL semantics), CookieCodec (KMS GenerateDataKey → AES-GCM seal of session id), CSRF double-submit helpers, TTL cache, per-session asyncio.Lock registry for refresh-storm coalescing, and CognitoRefreshClient (InitiateAuth REFRESH_TOKEN_AUTH with cached SECRET_HASH).
  • Adds SessionRefreshMiddleware and CSRFMiddleware registered on app-api in main.py. Both are registered unconditionally so a Phase 1 env-var deploy can flip them on without a code redeploy. They short-circuit when BFFConfig.is_enabled() is False (local dev, environments before Phase 1 deployed) and when no __Host-bff_session cookie is present (Bearer-token requests).
  • Adds get_current_user_from_session dependency in apis/shared/auth/dependencies.py for Phase 6 router cutover. No router consumes it yet — intentional per the Phase 2 scope.

Design choices worth flagging

  • Cookie name __Host-bff_session — the prefix forbids Domain= and requires Secure + Path=/, giving same-origin-only delivery for free once CloudFront /api/* lands in Phase 6.
  • Envelope encryption, cached DEK — at first use, kms:GenerateDataKey returns a 256-bit AES key; plaintext is cached in process memory, wrapped blob discarded. Matches the Phase 1 CDK comment. Key rotation requires a task restart, which is on the Phase 7 hardening list.
  • Refresh storm coalescing — per-session asyncio.Lock prevents N parallel tab refreshes from tumbling each other's refresh tokens via Cognito rotation. In-process only (Phase 7 will extend to multi-task with a DDB conditional write).
  • CSRF only enforced when a session is bound — Bearer-token requests bypass entirely, so this PR doesn't break the existing SPA.

Test plan

  • pytest tests/apis/shared/sessions_bff/ -v — 46 new tests all pass
    • Cookie codec: round-trip, tampered ciphertext rejected, garbage rejected, unknown-version rejected, wrong-key rejected
    • Repository: CRUD round-trip via moto, TTL-expired row treated as missing, disabled repo is inert
    • CSRF: helper validation, middleware exempts safe methods + Bearer requests, rejects mismatched/missing/forged tokens, exempts /auth/login|callback|logout
    • SessionRefreshMiddleware: pass-through when disabled, pass-through when no cookie, valid cookie attaches state, unrecoverable cookie cleared, refresh-failure cookie cleared
    • Storm coalescing test (the one called out in the migration plan): 5 concurrent requests against an httpx.AsyncClient(ASGITransport) for the same near-expiry session → exactly one Cognito refresh exchange
  • pytest tests/apis/shared/ tests/architecture/ — 92/92 pass, no import-boundary violations
  • pytest tests/apis/app_api/ tests/auth/ — 102/102 pass, no regressions from dependencies.py changes
  • python -c "from apis.app_api import main" — app-api imports cleanly with 4 middlewares registered (CORS → AgentCoreContext → SessionRefresh → CSRF)
  • No infrastructure or runtime change to verify in cloud — this PR is dormant code

🤖 Generated with Claude Code

Lands the server-side plumbing for the Bearer-to-cookie migration without
activating it. New `apis/shared/sessions_bff/` package (config, repository,
AES-GCM cookie codec backed by KMS GenerateDataKey, double-submit CSRF,
TTL cache, per-session refresh lock, Cognito refresh-token client) plus
`SessionRefreshMiddleware` and `CSRFMiddleware` registered on app-api.
Both middlewares are no-ops until a `__Host-bff_session` cookie is
present and `BFFConfig.is_enabled()` returns True, so this is safe to
ship before any caller flips on.

Also adds `get_current_user_from_session` dependency in `apis/shared/auth/dependencies.py`
for Phase 6 router cutover. No router consumes it yet.

Includes 46 tests covering codec round-trip + tamper rejection, CSRF
validation, repository CRUD with TTL semantics, middleware pass-through
when dormant, and a multi-tab refresh-token-storm coalescing test that
asserts N concurrent requests for the same session id drive exactly
one Cognito refresh exchange.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

import asyncio
from threading import Lock as _ThreadLock
from typing import Dict
try:
parsed = json.loads(value)
value = parsed.get("clientSecret") or parsed.get("client_secret") or value
except json.JSONDecodeError:
- Validate session-cookie tokens against COGNITO_BFF_APP_CLIENT_ID via a
  separate validator instance; the SPA validator's client_id check would
  reject every BFF-issued token at Phase 6 cutover.
- Bind the cookie version byte into AES-GCM associated data and stop
  swallowing KMS infrastructure errors as decode failures, so a transient
  KMS hiccup no longer logs every active user out.
- Correct the middleware-order comment in app_api/main.py (last-added is
  outermost) and add a Phase-6 marker on the CORS allow_credentials flag.
- TODO markers for the Cognito-rotation/DDB-write race and for the
  Phase-3 logout cache-invalidation gap.
- Tighten the CSRF tokens_match docstring to match actual constant-time
  semantics.
- Test updates: cover KMS-error propagation, assert both BFF cookies are
  cleared on unrecoverable cookies, and patch the new BFF validator name.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@philmerrell philmerrell merged commit 4aec51e into develop May 2, 2026
24 checks passed
@philmerrell philmerrell deleted the feature/bff-phase-2-backend-infra branch May 2, 2026 14:53
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.

2 participants