docs: rewrite seq-auth-flow.puml for the Spring Session model (ADR-020)
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m4s
CI / OCR Service Tests (pull_request) Successful in 20s
CI / Backend Unit Tests (pull_request) Successful in 3m6s
CI / fail2ban Regex (pull_request) Successful in 40s
CI / Semgrep Security Scan (pull_request) Successful in 17s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m0s

Removes the cookie-promotion step (auth_token → Authorization: Basic) and
splits the diagram into three labelled phases: Login, Authenticated
request, Logout. Adds the spring_session DB round-trip on every
authenticated request and the alt branch for an expired session
returning 401 → /login?reason=expired.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-17 20:59:44 +02:00
parent 0bd00a3044
commit 3438260090

View File

@@ -1,15 +1,21 @@
@startuml @startuml
title Authentication Flow (behind Caddy reverse proxy) title Authentication Flow (Spring Session JDBC, behind Caddy reverse proxy)
note over Browser, DB
Phase 1 of the auth rewrite (ADR-020 / #523).
Replaces the Basic-credentials-in-cookie model
with an opaque server-side session id (fa_session).
end note
actor User actor User
participant Browser participant Browser
participant "Caddy (TLS termination)" as Caddy participant "Caddy (TLS termination)" as Caddy
participant "Frontend (SvelteKit)" as Frontend participant "Frontend (SvelteKit)" as Frontend
participant "Backend (Spring Boot)" as Backend participant "Backend (Spring Boot)" as Backend
participant PostgreSQL as DB participant "spring_session\n(PostgreSQL)" as DB
== Login ==
User -> Browser: Enter email + password User -> Browser: Enter email + password
Browser -> Caddy: HTTPS POST /login (form action) Browser -> Caddy: HTTPS POST /?/login (form action)
note right of Caddy note right of Caddy
Caddy terminates TLS and forwards Caddy terminates TLS and forwards
to Frontend over HTTP with: to Frontend over HTTP with:
@@ -17,33 +23,54 @@ note right of Caddy
X-Forwarded-For: <client IP> X-Forwarded-For: <client IP>
X-Forwarded-Host: archiv.raddatz.cloud X-Forwarded-Host: archiv.raddatz.cloud
end note end note
Caddy -> Frontend: HTTP POST /login\n+ X-Forwarded-Proto: https Caddy -> Frontend: HTTP POST /?/login + X-Forwarded-Proto: https
Frontend -> Frontend: Base64 encode "email:password" Frontend -> Backend: POST /api/auth/login\n{email, password}\n+ X-Forwarded-Proto: https
Frontend -> Backend: GET /api/users/me\nAuthorization: Basic <token>\n+ X-Forwarded-Proto: https
note right of Backend note right of Backend
server.forward-headers-strategy: native server.forward-headers-strategy: native
Jetty's ForwardedRequestCustomizer → request.getScheme() = "https"
reads X-Forwarded-Proto so → Secure cookie flag set automatically.
request.getScheme() returns "https".
end note end note
Backend -> Backend: Spring Security parses Basic Auth Backend -> Backend: AuthenticationManager\nauthenticate(email, password)
Backend -> DB: SELECT user WHERE email=? Backend -> DB: SELECT user WHERE email=?
DB --> Backend: AppUser + groups + permissions DB --> Backend: AppUser + groups + permissions
Backend -> Backend: BCrypt.matches(password, hash) Backend -> Backend: BCrypt.matches(password, hash)\n(timing-safe: dummy hash on miss)
Backend --> Frontend: 200 OK — UserDTO Backend -> Backend: getSession(true).setAttribute(\n SPRING_SECURITY_CONTEXT, ctx)
Frontend -> Caddy: Set-Cookie: auth_token=<base64>\n(httpOnly, **Secure**, SameSite=strict, maxAge=86400) Backend -> DB: INSERT spring_session\n+ spring_session_attributes
note right of Frontend Backend -> Backend: AuditService.log(LOGIN_SUCCESS,\n {userId, ip, ua})
Secure flag is set because the Backend --> Frontend: 200 OK — AppUser\nSet-Cookie: fa_session=<opaque>;\n Path=/; HttpOnly; SameSite=Strict; Secure
request scheme observed by the Frontend -> Frontend: Parse Set-Cookie, re-emit fa_session\n(matches backend attrs)
app is https (forwarded by Caddy). Frontend --> Caddy: 303 → /\nSet-Cookie: fa_session=<opaque>
end note Caddy --> Browser: HTTPS 303 + Set-Cookie
Caddy -> Browser: HTTPS 200 + Set-Cookie
Browser -> Caddy: HTTPS GET / (next request) == Authenticated request ==
Caddy -> Frontend: HTTP GET / + X-Forwarded-Proto: https Browser -> Caddy: HTTPS GET /\nCookie: fa_session=<opaque>
Frontend -> Frontend: hooks.server.ts reads auth_token cookie Caddy -> Frontend: HTTP GET / + Cookie + X-Forwarded-Proto: https
Frontend -> Backend: GET /api/users/me\nAuthorization: Basic <token> Frontend -> Frontend: hooks.server.ts reads fa_session
Backend --> Frontend: 200 OK — user in event.locals Frontend -> Backend: GET /api/users/me\nCookie: fa_session=<opaque>
Frontend --> Caddy: rendered page Backend -> DB: SELECT * FROM spring_session\nWHERE SESSION_ID = ?
Caddy --> Browser: HTTPS 200 DB --> Backend: row (or null if expired)
alt Session valid
Backend -> DB: UPDATE spring_session\nSET LAST_ACCESS_TIME = now
Backend --> Frontend: 200 OK — AppUser
Frontend --> Caddy: rendered page
Caddy --> Browser: HTTPS 200
else Session expired (idle > 8h) or unknown
Backend --> Frontend: 401 Unauthorized
Frontend -> Frontend: hooks: delete fa_session cookie
Frontend --> Caddy: 302 → /login?reason=expired
Caddy --> Browser: HTTPS 302
end
== Logout ==
Browser -> Caddy: HTTPS POST /logout
Caddy -> Frontend: HTTP POST /logout\nCookie: fa_session=<opaque>
Frontend -> Backend: POST /api/auth/logout\nCookie: fa_session=<opaque>
Backend -> Backend: session.invalidate()\nSecurityContextHolder.clearContext()
Backend -> DB: DELETE FROM spring_session\nWHERE SESSION_ID = ?
Backend -> Backend: AuditService.log(LOGOUT,\n {userId, ip, ua})
Backend --> Frontend: 204 No Content
Frontend -> Frontend: cookies.delete('fa_session')
Frontend --> Caddy: 303 → /login
Caddy --> Browser: HTTPS 303 (cookie cleared)
@enduml @enduml