Skip to content

feat: Add JWT cookie-based authentication#167

Open
devin-ai-integration[bot] wants to merge 2 commits intoDevOpsfrom
devin/1776942265-jwt-authentication
Open

feat: Add JWT cookie-based authentication#167
devin-ai-integration[bot] wants to merge 2 commits intoDevOpsfrom
devin/1776942265-jwt-authentication

Conversation

@devin-ai-integration
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot commented Apr 23, 2026

Summary

Replaces Spring Security's built-in form login and session-based authentication with stateless JWT cookie-based authentication using jjwt 0.12.5.

New files:

  • JwtUtil – generates, parses, and validates HMAC-SHA256 JWTs. Secret is read from ${JWT_SECRET} env var (must be base64-encoded 256-bit key).
  • JwtAuthenticationFilterOncePerRequestFilter that reads a jwt cookie, validates it, and populates SecurityContextHolder.
  • AuthController – handles GET/POST /login and GET /logout, issuing/clearing the JWT cookie.

Modified files:

  • SecurityConfig – sessions set to STATELESS, formLogin/logout blocks removed, Spring Security's default LogoutFilter explicitly disabled, JWT filter inserted before UsernamePasswordAuthenticationFilter, unauthenticated requests redirect to /login.
  • BankController@GetMapping("/login") removed (moved to AuthController).
  • application.properties – added jwt.secret=${JWT_SECRET} and jwt.expiration-ms=3600000.
  • pom.xml – added jjwt-api, jjwt-impl, jjwt-jackson 0.12.5.

Updates since last revision

  • Fixed logout cookie clearing – Spring Security's built-in LogoutFilter was silently intercepting GET /logout before the AuthController could run, so the JWT cookie was never cleared. Fix: added .logout(logout -> logout.disable()) in SecurityConfig and switched the logout endpoint to return a ResponseEntity with an explicit Set-Cookie header via ResponseCookie, ensuring the cookie is reliably cleared on the redirect response.
  • Added /logout to the permitAll() matcher list.

Review & Testing Checklist for Human

  • Secure flag missing on JWT cookieAuthController sets HttpOnly but never calls cookie.setSecure(true) (login) or sets secure(true) (logout ResponseCookie). The cookie will be sent over plain HTTP. Verify whether HTTPS-only enforcement is acceptable and add the Secure flag if so.
  • CSRF exposure with cookie-based JWT – CSRF is disabled and sessions are stateless, but the JWT lives in an auto-sent cookie. Cross-site form submissions will carry the JWT. Consider adding SameSite=Strict on the cookie or re-enabling CSRF protection.
  • No server-side token revocation on logout – Logout only clears the client-side cookie. A copied/exfiltrated token remains valid for up to 1 hour. Decide if a token blacklist or shorter expiration is needed.
  • JWT_SECRET env var required at startupJwtUtil does Base64.getDecoder().decode(secret), so if the env var is unset or not valid base64, the app will crash on boot. Ensure all deployment environments (Docker Compose, K8s ConfigMap/Secret, local dev) have this variable configured.
  • DB hit on every requestJwtAuthenticationFilter calls accountService.loadUserByUsername() for each authenticated request, which may negate the performance benefit of stateless JWT.
  • Inconsistent logout pattern – The logout endpoint uses ResponseEntity with explicit headers while login uses response.addCookie() + Spring MVC redirect. This was necessary because Spring MVC's redirect view was not preserving the Set-Cookie header reliably. Verify both paths work in your deployment environment.

Suggested test plan:

  1. Set JWT_SECRET to a base64-encoded 256-bit key and start the app.
  2. GET /login → login page renders.
  3. POST /login with valid creds → response sets jwt HttpOnly cookie, redirects to /dashboard.
  4. GET /dashboard with cookie → authenticated view with user data.
  5. GET /dashboard without cookie → redirects to /login.
  6. GET /logout → cookie cleared (Set-Cookie: jwt=; Max-Age=0), redirects to /login?logout.
  7. GET /dashboard after logout → redirects to /login (cookie is gone).
  8. POST /register → still works and redirects to /login.

Notes

  • No unit or integration tests are included in this PR.
  • The validateToken method uses a broad catch (Exception e) which will swallow non-JWT errors silently.
  • Spring Security's default LogoutFilter must be disabled (.logout(logout -> logout.disable())) for custom JWT cookie logout to work. Without this, the filter intercepts /logout and performs its own redirect, discarding the cookie-clearing header.

Link to Devin session: https://app.devin.ai/sessions/6a04dcd6afda41a1a45265e1d3d4b9c3
Requested by: @isobina

- Add jjwt-api, jjwt-impl, jjwt-jackson dependencies (0.12.5)
- Add JWT secret (via env var) and expiration config in application.properties
- Create JwtUtil for token generation, extraction, and validation
- Create JwtAuthenticationFilter to authenticate requests via JWT cookie
- Create AuthController handling login/logout with JWT cookies
- Update SecurityConfig to stateless sessions with JWT filter
- Remove /login mapping from BankController (moved to AuthController)

Co-Authored-By: Ivan  Sobina <sobina.ivan@gmail.com>
@devin-ai-integration
Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

…on logout

Spring Security's built-in LogoutFilter was intercepting /logout requests
before the AuthController, preventing the JWT cookie from being cleared.
Fix disables the default LogoutFilter and uses ResponseEntity for explicit
control over the Set-Cookie header in the logout response.

Co-Authored-By: Ivan  Sobina <sobina.ivan@gmail.com>
@devin-ai-integration
Copy link
Copy Markdown
Author

End-to-End Test Results — JWT Authentication

Ran locally against MySQL Docker + JWT_SECRET. All 7 tests passed.

Devin session

Test Results (7/7 passed)
# Test Result
1 GET /login renders login page PASSED
2 GET /dashboard without auth redirects to /login PASSED
3 POST /login sets JWT cookie, redirects to /dashboard PASSED
4 GET /dashboard with JWT cookie shows user data PASSED
5 GET /logout clears cookie, redirects to /login?logout PASSED
6 GET /dashboard after logout redirects to /login PASSED
7 POST /register creates user, redirects to /login PASSED
JWT Cookie Verification (curl)
# Login response:
Set-Cookie: jwt=eyJhbGciOiJIUzI1NiJ9...; Max-Age=3600; Path=/; HttpOnly

# Logout response:
Set-Cookie: jwt=; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly
Screenshots

Login Page (Test 1)
Login

Dashboard After Login (Test 3)
Dashboard

Logout Redirect (Test 5)
Logout

Dashboard After Logout → Redirects to /login (Test 6)
After logout

Bug Found & Fixed

Spring Security's built-in LogoutFilter was silently intercepting GET /logout before the AuthController could run, so the JWT cookie was never cleared. Fixed in commit fa2592f by adding .logout(logout -> logout.disable()) in SecurityConfig and switching the logout endpoint to use ResponseEntity with an explicit Set-Cookie header.

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.

0 participants