Spring Boot
Implementing JWT Authentication in Spring Security
Step-by-step implementation of JWT-based authentication and authorization in Spring Boot applications, including refresh token handling and security best practices.
JSON Web Tokens (JWT) have become the standard for stateless authentication in modern web applications. This guide will walk you through implementing JWT authentication in a Spring Boot application.
Introduction to JWT
JWT is a compact, URL-safe means of representing claims to be transferred between two parties. It consists of three parts separated by dots:
- Header: Contains the type of token and signing algorithm
- Payload: Contains the claims (user data)
- Signature: Used to verify the token hasn't been tampered with
Why Use JWT?
- Stateless: No need to store sessions on the server
- Scalable: Perfect for microservices architecture
- Cross-domain: Can be used across different domains
- Mobile-friendly: Ideal for mobile applications
Spring Security Setup
First, add the necessary dependencies to your project:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</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>Application Properties
Configure JWT settings in your application.properties:
jwt.secret=mySecretKey
jwt.expiration=86400000
jwt.refresh.expiration=604800000JWT Utility Class
Create a utility class to handle JWT operations:
@Component
public class JwtUtils {
private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private int jwtExpirationMs;
@Value("${jwt.refresh.expiration}")
private int refreshTokenExpirationMs;
public String generateJwtToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
return generateTokenFromUsername(userPrincipal.getUsername());
}
public String generateTokenFromUsername(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String generateRefreshToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + refreshTokenExpirationMs))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String getUserNameFromJwtToken(String token) {
return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject();
}
public boolean validateJwtToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
logger.error("Invalid JWT signature: {}", e.getMessage());
} catch (MalformedJwtException e) {
logger.error("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
logger.error("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
logger.error("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
logger.error("JWT claims string is empty: {}", e.getMessage());
}
return false;
}
}Authentication Filter
Create a filter to process JWT tokens:
public class AuthTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsService userDetailsService;
private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
String username = jwtUtils.getUserNameFromJwtToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null,
userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7);
}
return null;
}
}Security Configuration
Configure Spring Security:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthEntryPointJwt unauthorizedHandler;
@Bean
public AuthTokenFilter authenticationJwtTokenFilter() {
return new AuthTokenFilter();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig)
throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeHttpRequests()
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated();
http.authenticationProvider(authenticationProvider());
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}Auth Controllers
Create authentication endpoints:
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
UserRepository userRepository;
@Autowired
PasswordEncoder encoder;
@Autowired
JwtUtils jwtUtils;
@PostMapping("/signin")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(),
loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtUtils.generateJwtToken(authentication);
String refreshToken = jwtUtils.generateRefreshToken(loginRequest.getUsername());
UserPrincipal userDetails = (UserPrincipal) authentication.getPrincipal();
List<String> roles = userDetails.getAuthorities().stream()
.map(item -> item.getAuthority())
.collect(Collectors.toList());
return ResponseEntity.ok(new JwtResponse(jwt, refreshToken,
userDetails.getId(),
userDetails.getUsername(),
userDetails.getEmail(),
roles));
}
@PostMapping("/signup")
public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signUpRequest) {
if (userRepository.existsByUsername(signUpRequest.getUsername())) {
return ResponseEntity.badRequest()
.body(new MessageResponse("Error: Username is already taken!"));
}
if (userRepository.existsByEmail(signUpRequest.getEmail())) {
return ResponseEntity.badRequest()
.body(new MessageResponse("Error: Email is already in use!"));
}
User user = new User(signUpRequest.getUsername(),
signUpRequest.getEmail(),
encoder.encode(signUpRequest.getPassword()));
Set<String> strRoles = signUpRequest.getRole();
Set<Role> roles = new HashSet<>();
if (strRoles == null) {
Role userRole = roleRepository.findByName(ERole.ROLE_USER)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(userRole);
} else {
strRoles.forEach(role -> {
switch (role) {
case "admin":
Role adminRole = roleRepository.findByName(ERole.ROLE_ADMIN)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(adminRole);
break;
default:
Role userRole = roleRepository.findByName(ERole.ROLE_USER)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(userRole);
}
});
}
user.setRoles(roles);
userRepository.save(user);
return ResponseEntity.ok(new MessageResponse("User registered successfully!"));
}
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@Valid @RequestBody TokenRefreshRequest request) {
String requestRefreshToken = request.getRefreshToken();
if (jwtUtils.validateJwtToken(requestRefreshToken)) {
String username = jwtUtils.getUserNameFromJwtToken(requestRefreshToken);
String token = jwtUtils.generateTokenFromUsername(username);
return ResponseEntity.ok(new TokenRefreshResponse(token, requestRefreshToken));
} else {
return ResponseEntity.badRequest()
.body(new MessageResponse("Refresh token is not valid!"));
}
}
}Testing Security
Create tests to verify your JWT implementation:
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class AuthControllerTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@Test
void shouldAuthenticateUser() {
LoginRequest loginRequest = new LoginRequest();
loginRequest.setUsername("testuser");
loginRequest.setPassword("password");
ResponseEntity<JwtResponse> response = restTemplate.postForEntity(
"/api/auth/signin", loginRequest, JwtResponse.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().getAccessToken()).isNotNull();
assertThat(response.getBody().getRefreshToken()).isNotNull();
}
@Test
void shouldRegisterNewUser() {
SignupRequest signupRequest = new SignupRequest();
signupRequest.setUsername("newuser");
signupRequest.setEmail("[email protected]");
signupRequest.setPassword("password123");
ResponseEntity<MessageResponse> response = restTemplate.postForEntity(
"/api/auth/signup", signupRequest, MessageResponse.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().getMessage()).contains("User registered successfully");
}
@Test
void shouldRefreshToken() {
// First, authenticate to get tokens
String refreshToken = authenticateAndGetRefreshToken();
TokenRefreshRequest refreshRequest = new TokenRefreshRequest();
refreshRequest.setRefreshToken(refreshToken);
ResponseEntity<TokenRefreshResponse> response = restTemplate.postForEntity(
"/api/auth/refresh", refreshRequest, TokenRefreshResponse.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().getAccessToken()).isNotNull();
}
}Best Practices
1. Secure Token Storage
- Store tokens securely on the client side
- Use HttpOnly cookies for web applications
- Implement proper token rotation
2. Token Validation
- Always validate tokens on the server side
- Check token expiration
- Verify token signature
3. Error Handling
@Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
logger.error("Unauthorized error: {}", authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
}
}4. Rate Limiting
Implement rate limiting to prevent brute force attacks:
@Component
public class LoginAttemptService {
private final int MAX_ATTEMPT = 3;
private LoadingCache<String, Integer> attemptsCache;
public LoginAttemptService() {
super();
attemptsCache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.DAYS)
.build(new CacheLoader<String, Integer>() {
public Integer load(String key) {
return 0;
}
});
}
public void loginSucceeded(String key) {
attemptsCache.invalidate(key);
}
public void loginFailed(String key) {
int attempts = 0;
try {
attempts = attemptsCache.get(key);
} catch (ExecutionException e) {
attempts = 0;
}
attempts++;
attemptsCache.put(key, attempts);
}
public boolean isBlocked(String key) {
try {
return attemptsCache.get(key) >= MAX_ATTEMPT;
} catch (ExecutionException e) {
return false;
}
}
}Conclusion
Implementing JWT authentication in Spring Security provides a robust, scalable solution for securing your applications. Key benefits include:
- Stateless authentication: No server-side session storage required
- Scalability: Perfect for distributed systems
- Flexibility: Works across different platforms and domains
- Security: Strong cryptographic signatures ensure token integrity
Remember to follow security best practices:
- Use strong secret keys
- Implement proper token expiration
- Add rate limiting
- Validate all tokens server-side
- Use HTTPS in production
With this implementation, you have a solid foundation for JWT-based authentication that can scale with your application's needs.