diff --git a/docs/architecture/c4/seq-auth-flow.puml b/docs/architecture/c4/seq-auth-flow.puml index 63b9038d..24d57e95 100644 --- a/docs/architecture/c4/seq-auth-flow.puml +++ b/docs/architecture/c4/seq-auth-flow.puml @@ -1,15 +1,21 @@ @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 participant Browser participant "Caddy (TLS termination)" as Caddy participant "Frontend (SvelteKit)" as Frontend participant "Backend (Spring Boot)" as Backend -participant PostgreSQL as DB +participant "spring_session\n(PostgreSQL)" as DB +== Login == User -> Browser: Enter email + password -Browser -> Caddy: HTTPS POST /login (form action) +Browser -> Caddy: HTTPS POST /?/login (form action) note right of Caddy Caddy terminates TLS and forwards to Frontend over HTTP with: @@ -17,33 +23,54 @@ note right of Caddy X-Forwarded-For: X-Forwarded-Host: archiv.raddatz.cloud end note -Caddy -> Frontend: HTTP POST /login\n+ X-Forwarded-Proto: https -Frontend -> Frontend: Base64 encode "email:password" -Frontend -> Backend: GET /api/users/me\nAuthorization: Basic \n+ X-Forwarded-Proto: https +Caddy -> Frontend: HTTP POST /?/login + X-Forwarded-Proto: https +Frontend -> Backend: POST /api/auth/login\n{email, password}\n+ X-Forwarded-Proto: https note right of Backend server.forward-headers-strategy: native - Jetty's ForwardedRequestCustomizer - reads X-Forwarded-Proto so - request.getScheme() returns "https". + → request.getScheme() = "https" + → Secure cookie flag set automatically. end note -Backend -> Backend: Spring Security parses Basic Auth +Backend -> Backend: AuthenticationManager\nauthenticate(email, password) Backend -> DB: SELECT user WHERE email=? DB --> Backend: AppUser + groups + permissions -Backend -> Backend: BCrypt.matches(password, hash) -Backend --> Frontend: 200 OK — UserDTO -Frontend -> Caddy: Set-Cookie: auth_token=\n(httpOnly, **Secure**, SameSite=strict, maxAge=86400) -note right of Frontend - Secure flag is set because the - request scheme observed by the - app is https (forwarded by Caddy). -end note -Caddy -> Browser: HTTPS 200 + Set-Cookie -Browser -> Caddy: HTTPS GET / (next request) -Caddy -> Frontend: HTTP GET / + X-Forwarded-Proto: https -Frontend -> Frontend: hooks.server.ts reads auth_token cookie -Frontend -> Backend: GET /api/users/me\nAuthorization: Basic -Backend --> Frontend: 200 OK — user in event.locals -Frontend --> Caddy: rendered page -Caddy --> Browser: HTTPS 200 +Backend -> Backend: BCrypt.matches(password, hash)\n(timing-safe: dummy hash on miss) +Backend -> Backend: getSession(true).setAttribute(\n SPRING_SECURITY_CONTEXT, ctx) +Backend -> DB: INSERT spring_session\n+ spring_session_attributes +Backend -> Backend: AuditService.log(LOGIN_SUCCESS,\n {userId, ip, ua}) +Backend --> Frontend: 200 OK — AppUser\nSet-Cookie: fa_session=;\n Path=/; HttpOnly; SameSite=Strict; Secure +Frontend -> Frontend: Parse Set-Cookie, re-emit fa_session\n(matches backend attrs) +Frontend --> Caddy: 303 → /\nSet-Cookie: fa_session= +Caddy --> Browser: HTTPS 303 + Set-Cookie + +== Authenticated request == +Browser -> Caddy: HTTPS GET /\nCookie: fa_session= +Caddy -> Frontend: HTTP GET / + Cookie + X-Forwarded-Proto: https +Frontend -> Frontend: hooks.server.ts reads fa_session +Frontend -> Backend: GET /api/users/me\nCookie: fa_session= +Backend -> DB: SELECT * FROM spring_session\nWHERE SESSION_ID = ? +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= +Frontend -> Backend: POST /api/auth/logout\nCookie: fa_session= +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