JWT TokensΒΆ
Comprehensive guide to JSON Web Token (JWT) authentication in the Spring Security Reference project. Learn how to generate, validate, and use JWTs for stateless authentication.
π― JWT OverviewΒΆ
JSON Web Tokens (JWTs) provide a stateless authentication mechanism that encodes user identity and claims in a cryptographically signed token.
sequenceDiagram
participant C as Client
participant A as Auth Endpoint
participant J as JWT Util
participant F as JWT Filter
participant R as Resource
Note over C,R: JWT Authentication Flow
C->>A: POST /api/auth/login<br/>username + password
A->>J: Generate JWT
J->>J: Create claims (sub, role, exp)
J->>J: Sign with secret key
J-->>A: Signed JWT Token
A-->>C: {"token": "eyJhbGci..."}
Note over C,R: Using JWT Token
C->>F: Request with Authorization: Bearer <token>
F->>J: Validate JWT signature
F->>J: Check token expiration
J-->>F: Claims extracted
F->>F: Create Authentication object
F->>R: Forward authenticated request
R-->>C: Protected resource π§ JWT ImplementationΒΆ
JwtTokenUtil ClassΒΆ
The JwtTokenUtil class handles JWT token generation and validation using cryptographically secure keys:
package com.example.spring.security.reference.commonauth;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
/**
* Utility class for JWT token creation and validation.
*
* Uses cryptographically secure keys for HS512 algorithm.
* The signing key is generated using Keys.secretKeyFor() which ensures
* the key is at least 512 bits (64 bytes) as required by HS512.
*/
@Component
public class JwtTokenUtil {
// Generate a secure 512-bit key for HS512 algorithm
// This prevents "WeakKeyException: The signing key's size is X bits
// which is not secure enough for the HS512 algorithm"
private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS512);
private static final long EXPIRATION_TIME = 86400000; // 24 hours (1 day)
/**
* Generate JWT token with user claims
*/
public String generateToken(String username, String role) {
return Jwts.builder()
.setSubject(username)
.claim("role", role)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
/**
* Extract claims from JWT token
*/
public Claims getClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
/**
* Validate JWT token against username
*/
public boolean validateToken(String token, String username) {
Claims claims = getClaimsFromToken(token);
return claims.getSubject().equals(username)
&& claims.getExpiration().after(new Date());
}
}
Production Consideration
The current implementation generates a new secret key on each application restart, which invalidates all previously issued tokens. In production, use a persistent secret key stored securely (e.g., environment variable, secrets manager).
JWT Authentication FilterΒΆ
The JwtAuthenticationFilter intercepts requests and validates JWT tokens:
package com.example.spring.security.reference.commonauth;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
/**
* Filter that authenticates JWT tokens for incoming requests.
*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String header = request.getHeader("Authorization");
String jwtToken = null;
String username = null;
// Extract JWT token from Authorization header
if (header != null && header.startsWith("Bearer ")) {
jwtToken = header.substring(7);
try {
Claims claims = jwtTokenUtil.getClaimsFromToken(jwtToken);
username = claims.getSubject();
String role = claims.get("role", String.class);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// Create authorities from role claim
List<SimpleGrantedAuthority> authorities = List.of(
new SimpleGrantedAuthority(role)
);
// Create authentication token and set in SecurityContext
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(username, null, authorities);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
} catch (Exception e) {
// Invalid token - continue without authentication
logger.debug("Invalid JWT token: " + e.getMessage());
}
}
chain.doFilter(request, response);
}
}
π― JWT Token StructureΒΆ
HeaderΒΆ
Payload (Claims)ΒΆ
| Claim | Description |
|---|---|
sub | Subject - the username |
role | User's role (e.g., ROLE_ADMIN, ROLE_USER) |
iat | Issued At - timestamp when token was created |
exp | Expiration - timestamp when token expires |
SignatureΒΆ
π Usage ExamplesΒΆ
1. Generate JWT TokenΒΆ
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin&password=password"
Response:
{
"token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJST0xFX0FETUlOIiwiaWF0IjoxNjk0NTIwMDAwLCJleHAiOjE2OTQ2MDY0MDB9.signature",
"username": "admin",
"role": "ROLE_ADMIN",
"message": "Login successful - use this JWT token for authenticated requests",
"usage": "Add header: Authorization: Bearer eyJhbGciOi..."
}
2. Use JWT Token for API AccessΒΆ
# Store token in variable
export JWT_TOKEN="eyJhbGciOiJIUzUxMiJ9..."
# Admin endpoint (requires ROLE_ADMIN)
curl -H "Authorization: Bearer $JWT_TOKEN" \
http://localhost:8080/api/admin/secure
# User endpoint (requires ROLE_USER or ROLE_ADMIN)
curl -H "Authorization: Bearer $JWT_TOKEN" \
http://localhost:8080/api/user/secure
# Auth info endpoint (any authenticated user)
curl -H "Authorization: Bearer $JWT_TOKEN" \
http://localhost:8080/api/auth/info
3. Test Different User RolesΒΆ
# Login as regular user
curl -X POST http://localhost:8080/api/auth/login \
-d "username=user&password=password"
# User token can access /api/user/** but NOT /api/admin/**
export USER_TOKEN="eyJhbGci..."
# This works (ROLE_USER can access user endpoints)
curl -H "Authorization: Bearer $USER_TOKEN" \
http://localhost:8080/api/user/secure
# This returns 403 Forbidden (ROLE_USER cannot access admin endpoints)
curl -H "Authorization: Bearer $USER_TOKEN" \
http://localhost:8080/api/admin/secure
π Security ConfigurationΒΆ
Adding JWT Filter to Security ChainΒΆ
The JWT filter is added before UsernamePasswordAuthenticationFilter in MultiAuthSecurityConfig:
@Configuration
@EnableWebSecurity
public class MultiAuthSecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain defaultFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/public/**", "/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
// JWT filter runs before username/password authentication
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
π Learning PointsΒΆ
| Concept | Description |
|---|---|
| Stateless Sessions | JWT enables stateless authentication - no server-side session storage needed |
| Filter Order | JWT filter must run early to authenticate before authorization checks |
| Claims | JWT payload contains user identity and role information |
| Signature | HMAC-SHA512 ensures token integrity and authenticity |
| Expiration | Tokens automatically expire after 24 hours (configurable) |
β οΈ Security Best PracticesΒΆ
- Use HTTPS: Always transmit JWT tokens over encrypted connections
- Secure Key Storage: Store signing keys in environment variables or secrets managers
- Short Expiration: Use shorter expiration times for sensitive applications
- Token Refresh: Implement refresh token mechanism for long-lived sessions
- Revocation: Consider implementing token blacklisting for logout functionality