JWT Authentication
with Spring Security

A complete visual guide to implementing stateless authentication — from user registration to token refresh. Built for interview preparation.

Complete Authentication Lifecycle
Step 1
Register
POST /api/user-register
One-time
Step 2
Login
POST /generate-token
Get Tokens
Step 3
Use API
GET /api/* + Bearer
Every Request
Step 4
Refresh
POST /refresh-token
On Expiry
All Components at a Glance
JWTAuthFilter
Handles login
JwtValidationFilter
Validates JWT per request
JWTRefreshFilter
Issues new access token
SecurityConfig
Wires everything
🔒
DaoAuthProvider
Username + Password
🔑
JWTAuthProvider
JWT token validation
👤
UserDetailsService
Loads user from DB
🔐
JWTUtil
Generate + Validate JWT
Filters Providers Services Utility
Complete Request Journey — Every API Call

This is exactly what happens when a client makes any authenticated API call

🌐
Client
Tomcat
Filter Chain
3 Custom Filters
ProviderManager
Routes by token type
🔒
AuthProvider
Dao or JWT
👤
UserDetails
Load from DB
🛡
SecurityContext
ThreadLocal
💻
Controller
Business Logic
📦
Response
Security Filter Chain

Every HTTP request passes through this chain top to bottom

1
JWTAuthenticationFilter
POST /generate-token only
2
JwtValidationFilter
Any request with Bearer header
3
JWTRefreshFilter
POST /refresh-token only
4
Authorization Check
permitAll() or authenticated()
Provider Routing (Strategy Pattern)

ProviderManager routes by token TYPE to the correct provider

Token Type
UsernamePasswordAuthenticationToken
{ username, password }
▼ routes to
DaoAuthenticationProvider
BCrypt verify • Used in Login (Step 2)
Token Type
JwtAuthenticationToken
{ jwt string }
▼ routes to
JWTAuthenticationProvider
Signature verify • Used in Validate (Step 3) & Refresh (Step 4)
Which Filter Handles Which Request?
Request Filter 1: Auth Filter 2: Validate Filter 3: Refresh Result
POST /api/user-register skip skip skip permitAll()
POST /generate-token HANDLES ★ skip skip Access + Refresh tokens
GET /api/* + Bearer skip HANDLES ★ skip SecurityContext set
POST /refresh-token skip skip HANDLES ★ New access token
Detailed System Architecture — All Components
SPRING SECURITY JWT SYSTEM ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ FILTERS │ │ PROVIDERS │ │ SERVICES │ │ UTILITY │ ├─────────────┤ ├─────────────┤ ├─────────────┤ ├─────────────┤ │ JWTAuth │ │ DaoAuth │ │ UserDetails │ │ JWTUtil │ │ Filter │ │ Provider │ │ Service │ │ │ │ (login) │ │ (user+pass) │ │ (DB bridge) │ │ generate() │ │ │ │ │ │ │ │ validate() │ │ JwtValid │ │ JWTAuth │ │ UserAuth │ │ │ │ Filter │ │ Provider │ │ Entity │ │ Password │ │ (API calls) │ │ (JWT token) │ │ Repository │ │ Encoder │ │ │ │ │ │ │ │ (BCrypt) │ │ JWTRefresh │ │ │ │ │ │ │ │ Filter │ │ │ │ │ │ │ │ (refresh) │ │ │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ ┌─────────────────────────────────────────────────────────────────┐ SecurityConfig — Wires all components, defines filter chain └─────────────────────────────────────────────────────────────────┘
Security Filter Chain — Detailed Execution Order
HTTP Request │ ▼ ┌──────────────────────────────────────────────────────────────────────┐ │ SECURITY FILTER CHAIN │ ┌────────────────────────────────────────────────────────────────┐ FILTER 1: JWTAuthenticationFilter Listens for: POST /generate-token Purpose: LOGIN — validate credentials, generate tokens All other URLs → doFilter() → pass through └──────────────────────────┬─────────────────────────────────────┘ ┌────────────────────────────────────────────────────────────────┐ FILTER 2: JwtValidationFilter Listens for: ANY request with Authorization: Bearer header Purpose: VALIDATE — verify JWT, set SecurityContext No header → doFilter() → pass through └──────────────────────────┬─────────────────────────────────────┘ ┌────────────────────────────────────────────────────────────────┐ FILTER 3: JWTRefreshFilter Listens for: POST /refresh-token Purpose: REFRESH — validate cookie, issue new access token All other URLs → doFilter() → pass through └──────────────────────────┬─────────────────────────────────────┘ ┌────────────────────────────────────────────────────────────────┐ Authorization Check (FilterSecurityInterceptor) permitAll() paths → allow authenticated() paths → check SecurityContext └──────────────────────────┬─────────────────────────────────────┘ └─────────────────────────────┼────────────────────────────────────────┘ │ ▼ DispatcherServlet → @Controller
Provider Routing — How ProviderManager Delegates (Strategy Pattern)
AuthenticationManager (ProviderManager) │ │ providers = [ DaoAuthenticationProvider, JWTAuthenticationProvider ] │ │ Receives Authentication object → checks TYPE → routes to correct provider │ │ ┌─────────────────────────────────┐ ┌───────────────────────────┐│ UsernamePasswordAuth Token │ │ JwtAuthenticationToken ││ { username="sj", password="123" │ │ { token="eyJhbG..." } ││ authenticated=false } │ │ authenticated=false } │└───────────────┬─────────────────┘ └──────────┬────────────────┘ │ │ │ │ ▼ ▼ │ ┌───────────────────────────────┐ ┌──────────────────────────────────┐DaoAuthenticationProvider JWTAuthenticationProvider│ │ │ │ supports(UsernamePassword supports(JwtAuth AuthToken) → TRUE ✓ Token) → TRUE ✓ 1. loadUserByUsername() 1. Extract token string → DB query 2. validateAndExtractUsername() 2. passwordEncoder.matches() → verify signature + expiry → BCrypt verify 3. loadUserByUsername() 3. Return authenticated token → DB query 4. Return authenticated token USED BY: Login (Step 2) └───────────────────────────────┘ USED BY: Validate (Step 3) Refresh (Step 4) └──────────────────────────────────┘
Fundamentals

What is JWT?

JSON Web Token — a self-contained, signed token that carries user identity. The server stays stateless.

JWT Structure — Three Parts Separated by Dots
eyJhbGciOiJIUzI1NiJ9
.
eyJzdWIiOiJzaiIsImlhdCI6MTcwMH0
.
WzAYgpAW6UNTL4nJ6GhFrW
■ HEADER (algorithm) ■ PAYLOAD (claims/data) ■ SIGNATURE (verification)
DECODED VIEW
Header
{
  "alg": "HS256",
  "typ": "JWT"
}
Payload
{
  "sub": "sj",
  "role": "ROLE_USER",
  "iat": 1700000000,
  "exp": 1700000900
}
Signature
HMACSHA256(
  base64(header) + "."
  + base64(payload),
  SECRET_KEY
)
Header

Metadata about the token

alg — signing algorithm (HS256)
typ — token type (JWT)

HS256 = HMAC-SHA256 (symmetric). RS256 = RSA (asymmetric).

Payload (Claims)

The actual data/claims

sub — subject (username)
iat — issued at (timestamp)
exp — expiration (timestamp)
role — custom claim
Base64 encoded, NOT encrypted! Anyone can decode and read. Never store passwords or secrets.
Signature

Integrity guarantee

HMACSHA256(
  base64(header) + "."
  + base64(payload),
  SECRET_KEY
)

If payload is tampered, re-computed signature won't match. Attacker can't forge without the secret key.

Tamper Detection — Why Signatures Matter

This is the #1 concept interviewers ask: "How does the server know if a JWT was modified?"

VALID TOKEN
Payload: { "sub": "sj", "role": "ROLE_USER" }
Original Sig: HMAC(h.p, secret) = abc123
Server Recomputes: HMAC(h.p, secret) = abc123
abc123 == abc123 ➔ SIGNATURE MATCHES ✓
TAMPERED TOKEN
Payload CHANGED: { "sub": "sj", "role": "ROLE_ADMIN" }
Original Sig: still abc123 (attacker can't recompute)
Server Recomputes: HMAC(h.p', secret) = xyz789
abc123 != xyz789 ➔ SIGNATURE MISMATCH ❌ REJECTED
Key Insight: The attacker can decode and modify the payload (it's just Base64), but cannot regenerate the signature without the SECRET_KEY. The server always recomputes the signature from the received header+payload and compares it.

🗃
Session-Based (Old Way)
Server stores sessions in memory (JSESSIONID cookie)
Server stores ALL user sessions in memory
Horizontal scaling needs sticky sessions / Redis
Bad for microservices (shared session store needed)
Bad for mobile apps (cookie handling complex)
CSRF vulnerable (cookies sent automatically)
Server restart = all sessions lost
VS
🔑
Token-Based / JWT (Modern)
Server is completely stateless — token carries everything
Server stores NOTHING — token is self-contained
Any server can validate (just needs the secret key)
Perfect for microservices (pass token between services)
Works great with mobile (header-based auth)
Header-based = immune to CSRF attacks
Server restart = no impact on existing tokens

Access Token vs Refresh Token
📄
Access Token
The "Key Card" to access APIs
Expiry15 minutes
Sent viaAuthorization Header
Sent onEvery API request
If stolen15 min damage window
StorageJS memory / localStorage
🔒
Refresh Token
The "Master Key" to get new access tokens
Expiry7 days
Sent viaHttpOnly Cookie
Sent onOnly /refresh-token
If stolenHttpOnly prevents JS theft
StorageHttpOnly Cookie (browser)
Step 1

User Registration

POST /api/user-register — Create user with BCrypt-hashed password. This endpoint is permitAll().

Registration Flow
Client
{ user, pass, role }
Controller
@PostMapping
PasswordEncoder
BCrypt.encode()
Service
save(entity)
Repository
JPA save()
Database
USER_AUTH table
BCrypt Password Hashing
// Input: "123"

// Output:
$2a$10$euzihUhyp4exMejDkyDb0eK2q49s...

$2a = algorithm (BCrypt)
$10 = cost factor (2^10 rounds)
22 chars = random salt
31 chars = hash output
Same password = different hash each time (random salt). Prevents rainbow table attacks.
UserAuthEntity ➔ Database
@Entity
class UserAuthEntity
  implements UserDetails

  id: Long
  username: String
  password: String (BCrypt)
  role: String
IDPASSWORDROLEUSERNAME
1 $2a$10$euzih... ROLE_USER sj
Why implement UserDetails? Spring Security only works with UserDetails objects during authentication. Implementing it avoids a separate mapping layer.
Complete Registration Flow — Detailed Diagram
POST /api/user-register { "username": "sj", "password": "123", "role": "ROLE_USER" } │ │ SecurityConfig: requestMatchers("/api/user-register").permitAll() │ → NO authentication needed, passes through all filters │ ▼ ┌──────────────────────────────────────────────────────────────────────┐ │ UserAuthController │ @RequestBody JSON ──► UserAuthEntity { username="sj", password="123", role="ROLE_USER", id=null } passwordEncoder.encode("123") ┌──────────────────────────────────────────────────────┐ BCryptPasswordEncoder 1. Generate random salt 2. hash = BCrypt("123" + salt, cost=10) 3. Return: $2a$10$euzihUhyp4exMejDkyDb0eK2q49s... algo cost salt hash └──────────────────────────────────────────────────────┘ entity.setPassword("$2a$10$euzih...") userAuthEntityService.save(entity) ┌──────────────────────────────────────────┐ │ UserAuthEntityRepository.save() → Hibernate generates: INSERT INTO user_auth (username, password, role) VALUES ('sj', '$2a$10$...', 'ROLE_USER') └──────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────┐ │ DATABASE: USER_AUTH │ │ ┌────┬───────────────────────┬───────────┬────────────┐ │ │ │ ID │ PASSWORD │ ROLE │ USERNAME │ │ │ ├────┼───────────────────────┼───────────┼────────────┤ │ │ │ 1 │ $2a$10$euzihUhyp4... │ ROLE_USER │ sj │ │ │ └────┴───────────────────────┴───────────┴────────────┘ │ └──────────────────────────────────────────────────────────┘ Response: 200 OK "User registered successfully!" └──────────────────────────────────────────────────────────────────────┘
Step 2

Login & Token Generation

POST /generate-token — Validate credentials via DaoAuthenticationProvider, then generate Access + Refresh tokens.

Authentication Flow
1. JWTAuthenticationFilter intercepts POST /generate-token
Parses JSON body with ObjectMapper (not @RequestBody — we're in a filter, not controller)
2. Creates UsernamePasswordAuthenticationToken
new UsernamePasswordAuthenticationToken("sj", "123")
➔ { principal="sj", credentials="123", authenticated=FALSE }
3. authenticationManager.authenticate(token)
ProviderManager iterates providers ➔ DaoAuthProvider.supports()? ➔ TRUE
4. DaoAuthenticationProvider validates
a) userDetailsService.loadUserByUsername("sj") ➔ DB query
b) passwordEncoder.matches("123", "$2a$10$...") ➔ BCrypt re-hash + compare
c) Check isEnabled, isNonLocked ➔ all true
5. Returns fully authenticated token
{ principal=UserAuthEntity, credentials=null (cleared!), authenticated=TRUE, authorities=[ROLE_USER] }
6. Generate tokens via JWTUtil
Access Token
Expiry: 15 min
Sent in: Response Header
Authorization: Bearer eyJ...
Refresh Token
Expiry: 7 days
Sent in: HttpOnly Cookie
Set-Cookie: refreshToken=eyJ...
Authentication Token State Transformation

Watch how the Authentication object evolves through the login process

BEFORE — Filter Creates
UsernamePasswordAuthToken
principal = "sj"
credentials = "123"
authenticated = FALSE
authorities = []
AFTER — Provider Returns
UsernamePasswordAuthToken
principal = UserAuthEntity {sj}
credentials = null (cleared!)
authenticated = TRUE
authorities = [ROLE_USER]
Why are credentials cleared? Security best practice — password should not linger in memory after authentication succeeds. Spring calls eraseCredentials() automatically.
HttpOnly Cookie Security Flags
HttpOnly
JS cannot read cookie.
Prevents XSS theft.
Secure
HTTPS only.
No plaintext interception.
Path
/refresh-token only.
Not sent on other requests.
MaxAge
604800 sec (7 days).
Auto-deleted after expiry.
Complete Login Flow — Detailed Diagram
POST /generate-token { "username": "sj", "password": "123" } │ ▼ ┌──── FILTER 1: JWTAuthenticationFilter ───────────────────────────────┐ URL == "/generate-token"? → YES ★ (handle it) ┌─── A: Parse Request Body ──────────────────────────────────┐ ObjectMapper.readValue(inputStream, LoginRequest.class) → LoginRequest { username="sj", password="123" } └────────────────────────────────────────────────────────────┘ ┌─── B: Create Auth Token ───────────────────────────────────┐ new UsernamePasswordAuthenticationToken("sj", "123") { principal="sj", credentials="123", authenticated=FALSE } └────────────────────────────────────────────────────────────┘ ┌─── C: authenticationManager.authenticate(authToken) ─────────┐ ProviderManager ├─ DaoAuthProvider.supports(UsernamePasswordToken)? │ → TRUE ★ │ │ │ ▼ ┌─────────────────────────────────────────────────┐ DaoAuthenticationProvider.authenticate() 1. userDetailsService.loadUserByUsername("sj") → repository.findByUsername("sj") SELECT * FROM user_auth WHERE username='sj' → UserAuthEntity { sj, $2a$10$..., ROLE_USER } 2. passwordEncoder.matches("123", "$2a$10$...") → extract salt → BCrypt("123"+salt) == stored? MATCH ✓ 3. Check: isEnabled, isNonLocked → all 4. Return: UsernamePasswordAuthenticationToken { principal=UserAuthEntity, credentials=null ← CLEARED authenticated=TRUE, authorities=[ROLE_USER] } └─────────────────────────────────────────────────┘ └───────────────────────────────────────────────────────────────┘ ┌─── D: authResult.isAuthenticated() → TRUE ─────────────────┐ ACCESS TOKEN: jwtUtil.generateToken("sj", 15) ┌──────────────────────────────────────────────────┐ │ Jwts.builder() │ │ .setSubject("sj") │ │ .setIssuedAt(now) │ │ .setExpiration(now + 15min) │ │ .signWith(key, HS256) ← HMAC signature │ .compact() │ │ → "eyJ.eyJ.Wz" │ └──────────────────────────────────────────────────┘ response.setHeader("Authorization", "Bearer " + token) REFRESH TOKEN: jwtUtil.generateToken("sj", 7 * 24 * 60) Cookie refreshCookie = new Cookie("refreshToken", token) ┌──────────────────────────────────────────────────┐ │ .setHttpOnly(true) ← JS cannot read │ .setSecure(true) ← HTTPS only │ .setPath("/refresh-token") ← only this endpoint │ .setMaxAge(604800) ← 7 days in seconds └──────────────────────────────────────────────────┘ response.addCookie(refreshCookie) └─────────────────────────────────────────────────────────────┘ └──────────────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────────┐ │ RESPONSE TO CLIENT: HTTP 200 OK Headers: Authorization: Bearer eyJ...(ACCESS TOKEN, 15min) Cookies: Set-Cookie: refreshToken=eyJ...(REFRESH TOKEN, 7days); HttpOnly; Secure; Path=/refresh-token Client stores access token in memory/localStorage Browser auto-manages refresh cookie (invisible to JS) └──────────────────────────────────────────────────────────────────────┘
Step 3

JWT Validation (Every API Call)

GET /api/* with Authorization: Bearer <token> — JwtValidationFilter extracts, validates, and sets SecurityContext.

Validation Flow
1. JwtValidationFilter extracts token from header
request.getHeader("Authorization")
➔ "Bearer eyJhbGciOi..."
➔ substring(7) ➔ "eyJhbGciOi..." (remove "Bearer " prefix)
2. Creates JwtAuthenticationToken
new JwtAuthenticationToken("eyJhbGci...")
➔ { token="eyJ...", authenticated=FALSE, principal=null }
3. ProviderManager routes to JWTAuthenticationProvider
DaoAuthProvider.supports(JwtAuthToken)? ➔ FALSE (skip)
JWTAuthProvider.supports(JwtAuthToken)? ➔ TRUE ★
4. JWTUtil.validateAndExtractUsername(token)
Verify Signature
HMAC(h.p, key) == sig?
✓ MATCH
Check Expiry
exp > now?
✓ NOT EXPIRED
Extract Subject
getSubject()
➔ "sj"
5. Load user from DB (fresh check)
userDetailsService.loadUserByUsername("sj") ➔ DB query ➔ user exists + not locked ✓
Why DB check again? User might have been deleted/disabled AFTER JWT was issued. JWT proves identity, DB gives current state.
6. Set SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(
  new UsernamePasswordAuthenticationToken(
    userDetails, null, [ROLE_USER]
  ) // ➔ authenticated=TRUE
);
Now @PreAuthorize, Controllers, and downstream code can access the authenticated user via SecurityContextHolder.
Token State Transformation
Before (Filter Creates)
JwtAuthenticationToken
token = "eyJhbG..."
principal = null
authenticated = FALSE
After (Provider Returns)
UsernamePasswordAuthToken
principal = UserAuthEntity {sj}
authorities = [ROLE_USER]
authenticated = TRUE
Complete Validation Flow — Detailed Diagram
GET /api/users Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzaiI... │ ▼ ┌──── FILTER 1: JWTAuthenticationFilter ───────────────────────────────┐ URL = "/api/users" ≠ "/generate-token" → filterChain.doFilter() PASS THROUGH └──────────────────────────────────────────────────────────────────────┘ │ ▼ ┌──── FILTER 2: JwtValidationFilter ──────────────────────────────────┐ ┌─── A: Extract JWT from Header ─────────────────────────────┐ request.getHeader("Authorization") → "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzaiI..." startsWith("Bearer ")? → YES substring(7) → "eyJhbGciOiJIUzI1NiJ9.eyJzdWI..." └─────────────────────────────────────────────────────────────┘ ┌─── B: Create JwtAuthenticationToken ────────────────────────┐ new JwtAuthenticationToken("eyJhbGci...") { token="eyJ...", authenticated=FALSE, principal=null } └─────────────────────────────────────────────────────────────┘ ┌─── C: authenticationManager.authenticate(jwtToken) ─────────┐ ProviderManager ├─ DaoAuthProvider.supports(JwtAuthToken)? → FALSE (skip) └─ JWTAuthProvider.supports(JwtAuthToken)? → TRUE ★ ┌─── JWTAuthenticationProvider.authenticate() ────────┐ 1. getToken() → "eyJhbGci..." 2. jwtUtil.validateAndExtractUsername(token) ┌──────────────────────────────────────────┐ │ Jwts.parser() │ │ .setSigningKey(key) │ │ .build().parseClaimsJws(token) │ │ │ │ │ ├─ Split: header.payload.signature │ ├─ Recalculate: │ │ │ HMAC(header.payload, key) │ ├─ Compare: calculated == received?│ │ │ → MATCH ✓ (not tampered) │ │ ├─ Check: exp > now? │ │ │ → YES ✓ (not expired) │ │ └─ Return: Claims object │ │ .getBody().getSubject() → "sj" └──────────────────────────────────────────┘ 3. username != null → valid 4. userDetailsService.loadUserByUsername("sj") → DB: SELECT * FROM user_auth WHERE username='sj' → UserAuthEntity { sj, ROLE_USER } 5. Return: UsernamePasswordAuthenticationToken { principal=UserAuthEntity, authenticated=TRUE, authorities=[ROLE_USER] } └──────────────────────────────────────────────────────┘ └───────────────────────────────────────────────────────────────┘ ┌─── D: Store in SecurityContext ─────────────────────────────┐ SecurityContextHolder.getContext().setAuthentication(result) SecurityContext (ThreadLocal for this request): ┌──────────────────────────────────────────┐ │ principal = UserAuthEntity {sj} │ authenticated = TRUE │ authorities = [ROLE_USER] └──────────────────────────────────────────┘ └───────────────────────────────────────────────────────────────┘ filterChain.doFilter(request, response) └──────────────────────────────────────────────────────────────────────┘ │ ▼ ┌──── Authorization Check ─────────────────────────────────────────────┐ anyRequest().authenticated() SecurityContext has authentication? → YES ✓ → ALLOWED └──────────────────────────────────────────────────────────────────────┘ │ ▼ ┌──── DispatcherServlet → @Controller ──────────────────────────────────┐ @GetMapping("/api/users") SecurityContextHolder.getContext().getAuthentication().getName()→"sj" └──────────────────────────────────────────────────────────────────────┘ │ ▼ RESPONSE: 200 OK + [{ user data }]
Step 4

Token Refresh

POST /refresh-token — Access token expired. Use the refresh cookie to get a new access token without re-login.

Refresh Flow
1. JWTRefreshFilter intercepts POST /refresh-token
Auth & Validation filters skip this request (not their URL, no Bearer header)
2. Extract refresh token from HttpOnly Cookie
Cookie[] cookies = request.getCookies();
// Loop: find cookie named "refreshToken"
➔ "eyJhbGci...(7-day JWT)"

// Browser sent cookie AUTOMATICALLY
// because Path=/refresh-token matches the URL
3. Validate via JWTAuthenticationProvider (same as Step 3)
JwtAuthenticationToken ➔ ProviderManager ➔ JWTAuthProvider
Verify signature ✓ • Check 7-day expiry ✓ • Load user from DB ✓
4. Generate NEW access token (15 min)
String newToken = jwtUtil.generateToken("sj", 15);
response.setHeader("Authorization", "Bearer " + newToken);

// Refresh cookie is NOT regenerated
// Original 7-day cookie continues until expiry
Complete Session Timeline
👤
Register
T=0
One-time
🔑
Login
T=0
Get both tokens
🚀
Use APIs
T=0 to T=15m
Bearer token
⚠️
Access Expires
T=15m
401 on API call
🔄
Refresh
T=15m
New access token
🔄
Repeat...
Every 15m
Until day 7
🛑
Refresh Expires
T=7 days
Must login again
Complete Refresh Flow — Detailed Diagram
ACCESS TOKEN EXPIRED (15 min passed) Client gets 401 on API call → triggers refresh POST /refresh-token Cookie: refreshToken=eyJhbGci...(7day token) (browser auto-sends cookie because Path=/refresh-token matches) (NO Authorization header — access token is expired) │ ▼ ┌──── FILTER 1: JWTAuthenticationFilter ───────────────────────────────┐ URL = "/refresh-token" ≠ "/generate-token" → PASS THROUGH └──────────────────────────────────────────────────────────────────────┘ │ ▼ ┌──── FILTER 2: JwtValidationFilter ──────────────────────────────────┐ request.getHeader("Authorization") → null (no access token) token = null → skip → PASS THROUGH └──────────────────────────────────────────────────────────────────────┘ │ ▼ ┌──── FILTER 3: JWTRefreshFilter ──────────────────────────────────────┐ URL == "/refresh-token"? → YES ★ (handle it) ┌─── A: Extract refresh token from COOKIE ───────────────────┐ request.getCookies() → Cookie[] { Cookie { name="JSESSIONID", value="..." }, Cookie { name="refreshToken", value="eyJhbGci...(7day JWT)" } ★ FOUND } for (cookie : cookies) "refreshToken".equals(cookie.getName())? → YES → return cookie.getValue() └─────────────────────────────────────────────────────────────┘ ┌─── C: Validate via AuthenticationManager ─────────────────────┐ new JwtAuthenticationToken(refreshToken) { token="eyJ...(7day)", authenticated=FALSE } ProviderManager ├─ DaoAuthProvider.supports(JwtAuthToken)? → FALSE (skip) └─ JWTAuthProvider.supports(JwtAuthToken)? → TRUE ★ JWTAuthenticationProvider.authenticate() ├── jwtUtil.validateAndExtractUsername(token) │ Verify signature → MATCH ✓ │ Check expiry → 7day, still valid ✓ │ Extract subject → "sj" ├── userDetailsService.loadUserByUsername("sj") │ → DB: user exists, not locked/disabled └── Return: { principal=sj, authenticated=TRUE, authorities=[ROLE_USER] } └───────────────────────────────────────────────────────────────┘ ┌─── D: Generate NEW access token ────────────────────────────┐ jwtUtil.generateToken("sj", 15)fresh 15 min token → "eyJhbGci...(BRAND NEW access token)" response.setHeader("Authorization", "Bearer " + newToken) NOTE: Refresh cookie NOT regenerated. Original 7-day cookie continues until it expires. └───────────────────────────────────────────────────────────────┘ └──────────────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────────┐ │ RESPONSE TO CLIENT: HTTP 200 OK Headers: Authorization: Bearer eyJ...(NEW ACCESS TOKEN, 15min) Client extracts new access token from header Retries the original failed API call with new token └──────────────────────────────────────────────────────────────────────┘