Files
familienarchiv/docs/architecture/c4/seq-auth-flow.puml
Marcel 05ab8b13a0 docs(arch): update auth sequence diagram to Phase 2 (CSRF, rate limit, revocation)
Extends the diagram from ADR-020 Phase 1 to cover:
- Rate limiter gate before credential validation in login
- CSRF double-submit cookie handshake for mutating requests
- Session revocation on password change (revokeOtherSessions) and
  password reset (revokeAllSessions)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 13:41:15 +02:00

5.5 KiB

Authentication Flow (Spring Session JDBC, behind Caddy reverse proxy)Authentication Flow (Spring Session JDBC, behind Caddy reverse proxy)UserBrowserCaddy .TLS termination.Frontend .SvelteKit.Backend .Spring Boot.LoginRateLimiterDBUserUserBrowserBrowserCaddy (TLS termination)Caddy (TLS termination)Frontend (SvelteKit)Frontend (SvelteKit)Backend (Spring Boot)Backend (Spring Boot)LoginRateLimiter(Caffeine+Bucket4j)LoginRateLimiter(Caffeine+Bucket4j)DBDBPhase 2 of the auth rewrite (ADR-020, ADR-022 / #523, #524).Adds CSRF double-submit cookies, login rate limiting, andsession revocation on password change/reset.Login (with rate limiting + CSRF bootstrap)Enter email + passwordHTTPS POST /?/login (form action)Caddy terminates TLS and forwardsto Frontend over HTTP with:X-Forwarded-Proto: httpsX-Forwarded-For: <client IP>X-Forwarded-Host: archiv.raddatz.cloudHTTP POST /?/login + X-Forwarded-Proto: httpsPOST /api/auth/login{email, password}+ X-Forwarded-Proto: httpsserver.forward-headers-strategy: native→ request.getScheme() = "https"→ Secure cookie flag set automatically.checkAndConsume(ip, email)[10/15min per ip+email; 20/15min per ip]alt[Rate limit exceeded]throw DomainException(TOO_MANY_LOGIN_ATTEMPTS)AuditService.log(LOGIN_RATE_LIMITED, {ip, email})429 Too Many Requests{"code":"TOO_MANY_LOGIN_ATTEMPTS"}Show rate-limit error[Under limit]AuthenticationManagerauthenticate(email, password)SELECT user WHERE email=?AppUser + groups + permissionsBCrypt.matches(password, hash)(timing-safe: dummy hash on miss)getSession(true).setAttribute(SPRING_SECURITY_CONTEXT, ctx)INSERT spring_session+ spring_session_attributesinvalidateOnSuccess(ip, email)AuditService.log(LOGIN_SUCCESS,{userId, ip, ua})200 OK — AppUserSet-Cookie: fa_session=<opaque>;Path=/; HttpOnly; SameSite=Strict; SecureSet-Cookie: XSRF-TOKEN=<token>;Path=/; SameSite=Strict; SecureParse Set-Cookie, re-emit fa_session(matches backend attrs)303 → /Set-Cookie: fa_session=<opaque>HTTPS 303 + Set-CookieAuthenticated mutating request (CSRF double-submit)handleFetch in hooks.client.ts reads the XSRF-TOKEN cookieand injects X-XSRF-TOKEN header on every POST/PUT/PATCH/DELETE.HTTPS POST /api/...Cookie: fa_session=<opaque>; XSRF-TOKEN=<token>X-XSRF-TOKEN: <token>HTTP POST /api/...+ Cookie + X-XSRF-TOKENalt[X-XSRF-TOKEN missing or mismatched]403 Forbidden{"code":"CSRF_TOKEN_MISSING"}HTTPS 403[CSRF valid]SELECT * FROM spring_session WHERE SESSION_ID = ?session rowProcess request2xx response + refreshed XSRF-TOKEN cookieHTTPS 2xxAuthenticated read requestHTTPS GET /Cookie: fa_session=<opaque>HTTP GET / + Cookie + X-Forwarded-Proto: httpshooks.server.ts reads fa_sessionGET /api/users/meCookie: fa_session=<opaque>SELECT * FROM spring_sessionWHERE SESSION_ID = ?row (or null if expired)alt[Session valid]UPDATE spring_sessionSET LAST_ACCESS_TIME = now200 OK — AppUserrendered pageHTTPS 200[Session expired (idle > 8h) or unknown]401 Unauthorizedhooks: delete fa_session cookie302 → /login?reason=expiredHTTPS 302Password change (revoke other sessions)POST /api/users/me/password{currentPassword, newPassword}+ X-XSRF-TOKENVerify currentPasswordUPDATE app_users SET password_hash = ?DELETE spring_session WHERE principal = ?AND session_id != <current>revokeOtherSessions: caller stays logged in,all other devices are signed out.204 No ContentPassword reset (revoke all sessions)POST /api/auth/reset-password{token, newPassword}Verify reset tokenUPDATE app_users SET password_hash = ?DELETE spring_session WHERE principal = ?revokeAllSessions: unauthenticated caller hasno session to preserve — all sessions wiped.204 No ContentLogoutHTTPS POST /logoutHTTP POST /logoutCookie: fa_session=<opaque>POST /api/auth/logoutCookie: fa_session=<opaque>session.invalidate()SecurityContextHolder.clearContext()DELETE FROM spring_sessionWHERE SESSION_ID = ?AuditService.log(LOGOUT,{userId, ip, ua})204 No Contentcookies.delete('fa_session')303 → /loginHTTPS 303 (cookie cleared)