Spring Boot Security: How to Implement JWT Authentication with Refresh Token


JSON Web Tokens (JWT) have become the industry standard for authentication in modern web applications. This guide walks through implementing JWT authentication in a Spring Boot application, adding a refresh token mechanism for enhanced security and a seamless user experience. You'll learn how to handle token creation, validation, refresh, and securing endpoints using Spring Security.
Prerequisites
Java 17 or higher
Maven/Gradle
Basic understanding of Spring Boot and Spring Security
Familiarity with JWT concepts
Project Setup
To begin, add the necessary dependencies to your pom.xml
file:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>
JWT Authentication with Refresh Token
1. Create the JwtUtil
Class
This utility class will handle JWT token generation, validation, and parsing. It will also manage the creation and validation of refresh tokens.
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
@Value("${jwt.refreshExpiration}")
private Long refreshExpiration;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
public String generateRefreshToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername(), refreshExpiration);
}
private String createToken(Map<String, Object> claims, String subject) {
return createToken(claims, subject, expiration);
}
private String createToken(Map<String, Object> claims, String subject, Long expirationTime) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(keyBytes);
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
}
2. Create the JwtRequestFilter
Class
This filter intercepts every HTTP request to check for the presence of the JWT token and validates it.
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
}
3. Create the SecurityConfig
Class
In this class, configure Spring Security to use JWT authentication and disable session management for stateless operations.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests()
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}
4. Create the AuthController
Class with Refresh Token Logic
This controller handles the login and token refresh requests, providing both JWT and refresh tokens.
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public ResponseEntity<?> createAuthenticationToken(
@RequestBody AuthenticationRequest authenticationRequest) {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
authenticationRequest.getUsername(),
authenticationRequest.getPassword()
)
);
} catch (BadCredentialsException e) {
throw new ResponseStatusException(
HttpStatus.UNAUTHORIZED, "Incorrect username or password");
}
final UserDetails userDetails = userDetailsService
.loadUserByUsername(authenticationRequest.getUsername());
final String jwt = jwtUtil.generateToken(userDetails);
final String refreshToken = jwtUtil.generateRefreshToken(userDetails);
return ResponseEntity.ok(new AuthenticationResponse(jwt, refreshToken));
}
@PostMapping("/refresh")
public ResponseEntity<?> refreshAuthenticationToken(
@RequestBody Map<String, String> refreshRequest) {
String refreshToken = refreshRequest.get("refreshToken");
if (refreshToken == null || !jwtUtil.validateToken(refreshToken, userDetailsService.loadUserByUsername(jwtUtil.extractUsername(refreshToken)))) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
final String username = jwtUtil.extractUsername(refreshToken);
final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
final String newJwt = jwtUtil.generateToken(userDetails);
return ResponseEntity.ok(new AuthenticationResponse(newJwt, refreshToken));
}
}
5. Application Properties
Add these properties to your application.properties
or application.yml
:
jwt.secret=your-256-bit-secret-key-here
jwt.expiration=86400000
jwt.refreshExpiration=2592000000 # 30 days
6. AuthenticationRequest and AuthenticationResponse Models
public class AuthenticationRequest {
private String username;
private String password;
// Getters and Setters
}
public class AuthenticationResponse {
private String jwt;
private String refreshToken;
public AuthenticationResponse(String jwt, String refreshToken) {
this.jwt = jwt;
this.refreshToken = refreshToken;
}
// Getters and Setters
}
Testing the Implementation
You can test your JWT and refresh token functionality using any HTTP client like Postman or cURL.
1. Login and get JWT + Refresh Token:
curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "user", "password": "password"}'
2. Refresh the JWT token:
curl -X POST http://localhost:8080/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken": "your-refresh-token-here"}'
Security Considerations
Secret Key: Always use a strong, randomly generated secret key of at least 256 bits.
Token Expiration: Set appropriate expiration times for both access and refresh tokens.
HTTPS: Use HTTPS in production to encrypt communication.
Token Storage: Store tokens securely on the client side (e.g., in HttpOnly cookies).
Refresh Tokens: Implement secure handling and rotation of refresh tokens.
Conclusion
This implementation provides a comprehensive JWT authentication system, including refresh token logic. It’s ideal for securing APIs while ensuring a smooth user experience through token management. Be sure to follow security best practices, including using strong secret keys and token expiration strategies, and thoroughly test your implementation before deploying it to production.
Subscribe to my newsletter
Read articles from Swarup Kanade directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
