JWT Authentication
AdvancedStateless authentication using signed JSON Web Tokens; a custom OncePerRequestFilter validates the token and populates the SecurityContext on each request.
Overview
JWT (JSON Web Token) enables stateless authentication: the server issues a signed token at login, and the client sends it with every subsequent request in the `Authorization: Bearer <token>` header. Because the token is self-contained (header.payload.signature), the server can verify it without a database lookup — making it ideal for horizontally-scaled microservices. In Spring Security, you implement a `OncePerRequestFilter` that extracts the token, validates the signature and expiry, and populates the `SecurityContextHolder`. Claims inside the JWT (sub, roles, exp) drive authorisation decisions. Key concerns: short expiry + refresh tokens, secure key management (RS256 over HS256 in multi-service environments), and token revocation strategies.
Issuing a JWT at Login
At login, validate credentials, then build and sign a JWT using the `jjwt` library (or `nimbus-jose-jwt`). The token contains claims: `sub` (subject/userId), `roles`, and `exp` (expiry). Return it in the response body; the client stores it in memory or a secure HTTP-only cookie.
@Service
class JwtService {
private final SecretKey key = Keys.hmacShaKeyFor(
Decoders.BASE64.decode(secretBase64)); // from config, min 256 bits
public String generate(User user) {
return Jwts.builder()
.subject(user.getId().toString())
.claim("roles", user.getRoles())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + 3_600_000)) // 1h
.signWith(key)
.compact();
}
public Claims validate(String token) {
return Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload(); // throws ExpiredJwtException, SignatureException on failure
}
}
@RestController
class AuthController {
@PostMapping("/auth/login")
Map<String, String> login(@RequestBody LoginRequest req) {
User user = authService.authenticate(req.email(), req.password());
return Map.of("token", jwtService.generate(user));
}
}Validating JWT on Every Request
Extend `OncePerRequestFilter` to intercept every request. Extract the `Authorization` header, strip "Bearer ", validate the JWT, build a `UsernamePasswordAuthenticationToken`, and set it in `SecurityContextHolder`. Configure Spring Security to be stateless and to use your filter before `UsernamePasswordAuthenticationFilter`.
@Component
class JwtAuthFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
String header = req.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
chain.doFilter(req, res); return;
}
try {
Claims claims = jwtService.validate(header.substring(7));
List<GrantedAuthority> auths = ((List<String>) claims.get("roles"))
.stream().map(SimpleGrantedAuthority::new).toList();
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(claims.getSubject(), null, auths);
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (JwtException e) {
res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token");
return;
}
chain.doFilter(req, res);
}
}
@Configuration
class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtFilter) throws Exception {
return http
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(a -> a
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated())
.csrf(AbstractHttpConfigurer::disable)
.build();
}
}Refresh Tokens and Revocation
Short-lived access tokens (15 min) reduce the window of a stolen token but require refresh tokens for seamless UX. A refresh token is a long-lived, opaque token stored server-side (Redis/DB) — it can be revoked. The client sends the refresh token to a `/auth/refresh` endpoint to get a new access token without re-logging-in. For true access-token revocation, use a Redis blocklist and check it in the JWT filter.
@Service
class RefreshTokenService {
private final RedisTemplate<String, String> redis;
// Refresh token: random UUID, stored in Redis with 30-day TTL
public String createRefreshToken(Long userId) {
String token = UUID.randomUUID().toString();
redis.opsForValue().set("refresh:" + token, userId.toString(),
Duration.ofDays(30));
return token;
}
public Long validateAndRotate(String refreshToken) {
String userId = redis.opsForValue().getAndDelete("refresh:" + refreshToken);
if (userId == null) throw new UnauthorizedException("Invalid/expired refresh token");
return Long.parseLong(userId);
}
}
// Blocklist for access token revocation (logout)
@PostMapping("/auth/logout")
void logout(@RequestHeader("Authorization") String bearerToken) {
String token = bearerToken.substring(7);
Claims claims = jwtService.validate(token);
Duration ttl = Duration.between(Instant.now(), claims.getExpiration().toInstant());
redis.opsForValue().set("blocklist:" + token, "1", ttl);
}
// In JwtAuthFilter — check blocklist
if (Boolean.TRUE.equals(redis.hasKey("blocklist:" + rawToken))) {
res.sendError(SC_UNAUTHORIZED, "Token revoked"); return;
}Key Points to Remember
- 1JWT is stateless: server validates the signature without a DB lookup — ideal for scaled microservices
- 2Claims: sub (userId), roles, exp (expiry) — all readable by any service with the key
- 3OncePerRequestFilter intercepts every request, validates token, sets SecurityContext
- 4SessionCreationPolicy.STATELESS prevents Spring from creating HTTP sessions
- 5Short access token (15 min) + long-lived refresh token (Redis) balances security and UX
- 6For true revocation: maintain a Redis blocklist and check it on every access-token validation
Interview Questions
Sign in to ask AriaHow does JWT enable stateless authentication and why is it useful in microservices?
Where should a JWT be stored on the client — localStorage or HTTP-only cookie?
How would you implement JWT revocation when JWTs are stateless?
What is the purpose of the refresh token and why should it be stored server-side?
What is the difference between HS256 and RS256 signing and when would you prefer RS256?
Ask Aria about JWT Authentication
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.