Skip to content

Auth: wire Spring Security + Spring Session JDBC end to end#47

Open
RandyJDean wants to merge 1 commit into
07-09-auth_request-link_and_verify_flowsfrom
07-09-auth_wire_spring_security_spring_session_jdbc_end_to_end
Open

Auth: wire Spring Security + Spring Session JDBC end to end#47
RandyJDean wants to merge 1 commit into
07-09-auth_request-link_and_verify_flowsfrom
07-09-auth_wire_spring_security_spring_session_jdbc_end_to_end

Conversation

@RandyJDean

Copy link
Copy Markdown
Contributor
  • 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

- 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>

RandyJDean commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

@sonarqubecloud

sonarqubecloud Bot commented Jul 9, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
D Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

💡 Need a hand with PR review? Try Gitar by Sonar!

}

private HttpSecurity common(final HttpSecurity http) throws Exception {
return http.csrf(AbstractHttpConfigurer::disable)
@RandyJDean RandyJDean marked this pull request as ready for review July 10, 2026 18:47
@RandyJDean RandyJDean requested a review from a team as a code owner July 10, 2026 18:47
Comment on lines +76 to +78
.orElseGet(() -> {
invalidateSession(httpRequest);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ApiResponder.failure("Not signed in"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
.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

Fix in 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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to add an example of how to protect an endpoint.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants