Auth: wire Spring Security + Spring Session JDBC end to end#47
Conversation
- AuthController: POST /api/auth/request-link|verify|logout and GET /api/session; verify performs programmatic login (session id rotation + SecurityContextRepository save), session reads the member fresh so profile completion is never stale - SecurityConfig moved to auth/security and rewired: patchats_session cookie (HttpOnly, SameSite=Lax, Secure per profile, 30d) via DefaultCookieSerializer; dev/prod chains share CSRF-off posture documented in the javadoc; prod email rule kept fail-closed - ApiAuthenticationEntryPoint answers 401s in the ApiResponder envelope - AuthenticatedMember principal is Serializable (persisted by Spring Session) and exposes email as the indexed principal name - SecurityWiringTest exercises the real filter chain: anonymous 401, session-carried auth across requests, logout invalidation Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
|
| .orElseGet(() -> { | ||
| invalidateSession(httpRequest); | ||
| return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ApiResponder.failure("Not signed in")); |
There was a problem hiding this comment.
Missing SecurityContextHolder.clearContext() call when member not found. If a member is deleted from the database while their session is still active, the session gets invalidated but the SecurityContext remains in the thread-local for the duration of this request. This could allow subsequent code in the same request to still see an authenticated principal that should no longer exist.
.orElseGet(() -> {
invalidateSession(httpRequest);
SecurityContextHolder.clearContext(); // Add this line
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ApiResponder.failure("Not signed in"));
});Note that the logout() method at line 86 correctly clears the context after invalidation.
| .orElseGet(() -> { | |
| invalidateSession(httpRequest); | |
| return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ApiResponder.failure("Not signed in")); | |
| .orElseGet(() -> { | |
| invalidateSession(httpRequest); | |
| SecurityContextHolder.clearContext(); | |
| return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ApiResponder.failure("Not signed in")); | |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
| /** Answers unauthenticated requests to protected endpoints with a 401 in the standard JSON envelope. */ | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class ApiAuthenticationEntryPoint implements AuthenticationEntryPoint { |
There was a problem hiding this comment.
Would be good to add an example of how to protect an endpoint.



GET /api/session; verify performs programmatic login (session id
rotation + SecurityContextRepository save), session reads the member
fresh so profile completion is never stale
cookie (HttpOnly, SameSite=Lax, Secure per profile, 30d) via
DefaultCookieSerializer; dev/prod chains share CSRF-off posture
documented in the javadoc; prod email rule kept fail-closed
Session) and exposes email as the indexed principal name
session-carried auth across requests, logout invalidation
Co-Authored-By: Claude Fable 5 noreply@anthropic.com