feat(authenticator): /auth/refresh rotation + session management + CSRF (nginx+auth step 10, PR 1/4) - #1848
Conversation
…ace (steps 10.1–10.2)
Item 1 — POST /auth/refresh (G10 rotation model, no swap keys):
a fresh CSPRNG token mapping is written and the superseded mapping's TTL
drops to refresh_grace_ms (default 250 ms) — the expiring old mapping IS the
grace window — while the session's expires_at advances to min(now +
session_ttl, absolute cap) across the record, its key TTL, and the per-user
index score, all in one pipeline. The stable session_id and the linked JWT
are untouched. A stale token inside the grace window resolves to the same
session and is answered with the current credential (no re-rotation); past
grace or past either cap → 401 + cleared cookie. Response is
{expires_at, refresh_at} with refresh_at = expires_at − 90 s ± uniform(60 s)
(big-jitter decision, G8), re-jittered per call and shared with GET /auth/me;
cookie Max-Age is the actual remaining session life.
Item 2 — session management:
GET /auth/sessions lists the caller's live sessions from the per-user ZSET
(created_at, expires_at, user_agent, ip, current flag; attribution captured
at login from User-Agent + first X-Forwarded-For hop, length-capped).
DELETE /auth/sessions/{id} revokes one owned session (absent and not-owned
are both 404 — no existence oracle); DELETE /auth/sessions revokes everything
for the current user. Every revoke runs the standard pipeline: token mappings
+ session + linked JWT + index entries in one MULTI/EXEC.
The admin/service variant DELETE /auth/admin/users/{person_id}/sessions is a
.authenticated() operation: the host authn pipeline (cf-gears-oidc-authn-plugin,
newly linked) verifies the ES256 gateway JWT against the authenticator's own
issuer, and the handler requires one of admin_revoke_roles (default
["session_admin"]) before delegating to the SDK contract
(AuthenticatorClientV1::revoke_user_sessions) — the lever the future
permissions service pulls (DD-AUTH-07).
Config wiring: committed host config flips to auth_disabled: false with a
fail-closed .invalid placeholder issuer; dev compose bind-mounts a full-auth
override (authn-tls issuer + self-signed CA) and grants the dev testclient
the session_admin role; the Helm configmap renders the plugin block off
tlsDiscovery (real issuer + in-pod CA when enabled, dark otherwise).
Also repairs the e2e harness: identity-stub readiness probed the old
/v1/persons path, and two stale ignored e2e asserts predated the
space-delimited roles claim and the UUIDv5 service sub. run-e2e.sh now also
runs the new refresh + sessions loops; all four e2e loops pass locally.
EPIC: constructorfabric#1583 (step 10, constructorfabric#1593)
Signed-off-by: Anton Zelenov <antonz@constructor.tech>
Second line behind SameSite=Strict (PRD 5.11 / DESIGN 4.2, salvaged spec): middleware over the route table checks POST/PUT/PATCH/DELETE under /auth/*. X-CSRF-Token is compared in constant time (fixed-size SHA-256 digests) against the per-session token minted at login; with no header, the Origin allowlist (csrf_origins) is the fallback; empty allowlist (the default) is fail closed — token required. A presented-but-wrong token is never rescued by the Origin fallback. Requests without a resolvable session pass through (the handler answers 401 — nothing to forge); a Redis failure answers 503, never a bypass. The back-channel logout endpoint is exempt: IdP server-to-server, its credential is the signed logout_token. GET /auth/csrf issues the session's token; /auth/me echoes it so one call primes both the refresh timer and the CSRF header at page load. Config: csrf_origins in the committed config (empty = fail closed), the dev compose override (Vite + gateway origins keep browser flows working until the SPA header lands everywhere), and a new chart value csrfOrigins. The SPA side (store csrf_token from /auth/me, send X-CSRF-Token on logout) lands in insight-front (feat/auth-csrf-header). e2e now asserts 403-without / pass-with the header on refresh and log-out-everywhere; all loops green. EPIC: constructorfabric#1583 (step 10, constructorfabric#1593) Signed-off-by: Anton Zelenov <antonz@constructor.tech>
M2 (QA/security review): make /auth/refresh rotation a compare-and-swap on the session's current_token (atomic Lua) instead of an unconditional pipeline. Two concurrent refreshes of the same cookie (multi-tab, within the refresh burst) could both pass the grace check and both rotate, leaving the first new token mapping written with the full session TTL and never demoted to grace — a live parallel credential that revoke never touches. The CAS lets only one rotate; the loser re-loads and answers the grace path with the winner's current credential. No orphan mapping. M5: document the fail-closed CSRF deploy coordination in the chart values — roll the header-sending insight-front first, or set csrfOrigins, or logout/ refresh 403 during the transition. EPIC: constructorfabric#1583 (step 10, constructorfabric#1593) Signed-off-by: Anton Zelenov <antonz@constructor.tech>
|
Warning Review limit reached
Next review available in: 11 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (20)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Superseded — the four stacked step-10 PRs are collapsed into a single PR (#1851). Same commits, one review, one merge. Closing this subset. |
Step 10 (auth surface completion) of the nginx+auth EPIC, PR 1 of 4. Delivers items 10.1 (
POST /auth/refresh), 10.2 (session management), and 10.5 (CSRF) ofNGINX_BFF_10.md.This is the base of a 4-PR stack; the others build on it:
GitHub can't base a cross-fork PR on another fork branch, so all four target
main; review this one first (the later PRs' diffs include these commits until this merges).What's here
POST /auth/refresh— rotation with grace (G10). New CSPRNG token mapping; the superseded mapping's TTL drops torefresh_grace_ms(250 ms) — the expiring old mapping is the grace window (no swap keys).expires_at = min(now + session_ttl, absolute cap)across the record, key TTL, and per-user ZSET score. Stablesession_id+ linked JWT untouched. Stale-in-grace resolves to the same session (no re-rotation); past grace or either cap → 401 + clear cookie. Response{expires_at, refresh_at},refresh_at = expires_at − 90 s ± 60 s(big-jitter, G8), re-jittered per call and shared with/auth/me.GET /auth/sessions(created_at, expires_at, user_agent, ip, current — attribution captured at login);DELETE /auth/sessions/{id}(no-existence-oracle 404);DELETE /auth/sessions; adminDELETE /auth/admin/users/{person_id}/sessions— an.authenticated()op (host authn pipeline verifies the gateway JWT against the authenticator's own issuer), role-gated byadmin_revoke_roles(defaultsession_admin), delegating to the SDKAuthenticatorClientV1::revoke_user_sessions. Every revoke: token mappings + session + linked JWT + indexes in one pipeline./auth/*: constant-timeX-CSRF-Tokenvs the session record,Origin-allowlist fallback (csrf_origins, empty = fail closed), back-channel exempt.GET /auth/csrf;/auth/meechoes the token. SPA side is the insight-front PR.Review-fix commit
The last commit applies M2 (rotation is now a compare-and-swap Lua on
current_token— concurrent multi-tab refreshes can't both rotate and orphan a full-TTL credential) and M5 (CSRF fail-closed deploy-coordination note) from the security + QA review of the stack.Testing
Unit tests + clippy clean. Local e2e harness (
services/authenticator/tests/run-e2e.sh, 8 loops against fakeidp + Redis) is green on the full stack; the refresh loop asserts rotation, sid-stability, grace resolution, and past-grace 401.csrfOrigins, or logout/refresh will 403 during the transition (documented in the chart values).EPIC: #1583 · closes the step-10 items in #1593 (partial — see the stack)
🤖 Generated with Claude Code