Home/Learn/Spring Boot/JWT Authentication

JWT Authentication

Advanced
Security

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

Spring Boot — JWT issue at login (jjwt)
@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`.

Spring Security — OncePerRequestFilter JWT validation
@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.

Spring Boot — refresh tokens and Redis blocklist
@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 Aria
1

How does JWT enable stateless authentication and why is it useful in microservices?

EasyAmazon
2

Where should a JWT be stored on the client — localStorage or HTTP-only cookie?

MediumThoughtWorks
3

How would you implement JWT revocation when JWTs are stateless?

HardNetflix
4

What is the purpose of the refresh token and why should it be stored server-side?

MediumOkta
5

What is the difference between HS256 and RS256 signing and when would you prefer RS256?

HardAuth0

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.

Loading discussion…