Introduction to JWT Authentication
JSON Web Tokens (JWT) have become the standard for stateless authentication in modern web applications. JWT provides a compact, URL-safe way to represent claims between two parties, making it ideal for RESTful APIs and microservices architectures.
JWT Structure
A JWT consists of three parts:
- Header: Contains token type and signing algorithm
- Payload: Contains claims (user data, expiration, etc.)
- Signature: Verifies the token integrity
Project Setup and Dependencies
Maven Dependencies
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"> <dependencies> <!-- Spring Boot Starter Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Boot Starter Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- JWT Library --> <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> <!-- Database (Optional) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies> </project>
Core JWT Components
1. JWT Utility Class
package com.example.security.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtTokenUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
// Generate secret key from the secret string
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
// Retrieve username from jwt token
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
// Retrieve expiration date from jwt token
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
// For retrieving any information from token we will need the secret key
private Claims getAllClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
// Check if the token has expired
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
// Generate token for user
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
}
// While creating the token -
// 1. Define claims of the token, like Issuer, Expiration, Subject, and the ID
// 2. Sign the JWT using the HS512 algorithm and secret key
// 3. According to JWS Compact Serialization, compaction of the JWT to a URL-safe string
private String doGenerateToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(getSigningKey(), SignatureAlgorithm.HS512)
.compact();
}
// Validate token
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
// Refresh token
public String refreshToken(String token) {
final Date createdDate = new Date();
final Date expirationDate = calculateExpirationDate(createdDate);
final Claims claims = getAllClaimsFromToken(token);
claims.setIssuedAt(createdDate);
claims.setExpiration(expirationDate);
return Jwts.builder()
.setClaims(claims)
.signWith(getSigningKey(), SignatureAlgorithm.HS512)
.compact();
}
private Date calculateExpirationDate(Date createdDate) {
return new Date(createdDate.getTime() + expiration * 1000);
}
}
2. Custom User Details Service
package com.example.security.service;
import com.example.security.model.User;
import com.example.security.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
List<SimpleGrantedAuthority> authorities = Collections.singletonList(
new SimpleGrantedAuthority("ROLE_" + user.getRole())
);
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
authorities
);
}
}
3. JWT Authentication Filter
package com.example.security.jwt;
import com.example.security.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
// JWT Token is in the form "Bearer token"
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
logger.error("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
logger.warn("JWT Token has expired");
} catch (Exception e) {
logger.error("JWT Token validation failed");
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
}
// Once we get the token validate it
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// If token is valid, configure Spring Security to manually set authentication
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// After setting the Authentication in the context, we specify
// that the current user is authenticated
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
}
Security Configuration
Spring Security Configuration
package com.example.security.config;
import com.example.security.jwt.JwtAuthenticationEntryPoint;
import com.example.security.jwt.JwtAuthenticationFilter;
import com.example.security.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.cors().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.antMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Add JWT token filter
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
JWT Authentication Entry Point
package com.example.security.jwt;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException, ServletException {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getOutputStream().println(
"{ \"error\": \"" + authException.getMessage() + "\" }"
);
}
}
Data Models and Repository
User Entity
package com.example.security.model;
import javax.persistence.*;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String role = "USER";
// Constructors
public User() {}
public User(String username, String password, String email, String role) {
this.username = username;
this.password = password;
this.email = email;
this.role = role;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
}
User Repository
package com.example.security.repository;
import com.example.security.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
Boolean existsByUsername(String username);
Boolean existsByEmail(String email);
}
Authentication Controller
Authentication Request/Response DTOs
package com.example.security.dto;
public class AuthenticationRequest {
private String username;
private String password;
// Default constructor for JSON parsing
public AuthenticationRequest() {}
public AuthenticationRequest(String username, String password) {
this.username = username;
this.password = password;
}
// Getters and Setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
package com.example.security.dto;
public class AuthenticationResponse {
private String token;
private String type = "Bearer";
private String username;
private String role;
public AuthenticationResponse(String token, String username, String role) {
this.token = token;
this.username = username;
this.role = role;
}
// Getters and Setters
public String getToken() { return token; }
public void setToken(String token) { this.token = token; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
}
package com.example.security.dto;
public class RegisterRequest {
private String username;
private String password;
private String email;
private String role = "USER";
// Constructors, Getters and Setters
public RegisterRequest() {}
public RegisterRequest(String username, String password, String email) {
this.username = username;
this.password = password;
this.email = email;
}
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
}
Authentication Controller
package com.example.security.controller;
import com.example.security.dto.AuthenticationRequest;
import com.example.security.dto.AuthenticationResponse;
import com.example.security.dto.RegisterRequest;
import com.example.security.jwt.JwtTokenUtil;
import com.example.security.model.User;
import com.example.security.repository.UserRepository;
import com.example.security.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/auth")
public class AuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping("/login")
public ResponseEntity<?> createAuthenticationToken(
@RequestBody @Valid AuthenticationRequest authenticationRequest) {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
authenticationRequest.getUsername(),
authenticationRequest.getPassword()
)
);
} catch (BadCredentialsException e) {
Map<String, String> error = new HashMap<>();
error.put("error", "Incorrect username or password");
return ResponseEntity.badRequest().body(error);
}
final UserDetails userDetails = userDetailsService
.loadUserByUsername(authenticationRequest.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
// Get user role
User user = userRepository.findByUsername(authenticationRequest.getUsername())
.orElseThrow(() -> new RuntimeException("User not found"));
return ResponseEntity.ok(new AuthenticationResponse(
token, user.getUsername(), user.getRole()
));
}
@PostMapping("/register")
public ResponseEntity<?> registerUser(@RequestBody @Valid RegisterRequest registerRequest) {
// Check if username exists
if (userRepository.existsByUsername(registerRequest.getUsername())) {
Map<String, String> error = new HashMap<>();
error.put("error", "Username is already taken");
return ResponseEntity.badRequest().body(error);
}
// Check if email exists
if (userRepository.existsByEmail(registerRequest.getEmail())) {
Map<String, String> error = new HashMap<>();
error.put("error", "Email is already in use");
return ResponseEntity.badRequest().body(error);
}
// Create new user
User user = new User(
registerRequest.getUsername(),
passwordEncoder.encode(registerRequest.getPassword()),
registerRequest.getEmail(),
registerRequest.getRole()
);
userRepository.save(user);
Map<String, String> response = new HashMap<>();
response.put("message", "User registered successfully");
return ResponseEntity.ok(response);
}
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestHeader("Authorization") String authHeader) {
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
String refreshedToken = jwtTokenUtil.refreshToken(token);
Map<String, String> response = new HashMap<>();
response.put("token", refreshedToken);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, String> error = new HashMap<>();
error.put("error", "Invalid token");
return ResponseEntity.badRequest().body(error);
}
}
Map<String, String> error = new HashMap<>();
error.put("error", "Authorization header is missing or invalid");
return ResponseEntity.badRequest().body(error);
}
}
Protected Resources and Controllers
User Controller
package com.example.security.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/user")
public class UserController {
@GetMapping("/profile")
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
public ResponseEntity<?> getUserProfile() {
Map<String, String> profile = new HashMap<>();
profile.put("message", "This is user profile data");
profile.put("access", "User level access granted");
return ResponseEntity.ok(profile);
}
@GetMapping("/dashboard")
@PreAuthorize("hasRole('USER')")
public ResponseEntity<?> getUserDashboard() {
Map<String, String> dashboard = new HashMap<>();
dashboard.put("message", "Welcome to User Dashboard");
dashboard.put("features", "Basic features available");
return ResponseEntity.ok(dashboard);
}
}
Admin Controller
package com.example.security.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/admin")
public class AdminController {
@GetMapping("/dashboard")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> getAdminDashboard() {
Map<String, String> dashboard = new HashMap<>();
dashboard.put("message", "Welcome to Admin Dashboard");
dashboard.put("features", "All administrative features available");
return ResponseEntity.ok(dashboard);
}
@GetMapping("/users")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> getAllUsers() {
Map<String, String> users = new HashMap<>();
users.put("message", "List of all users (admin only)");
users.put("data", "User data would be here");
return ResponseEntity.ok(users);
}
}
Public Controller
package com.example.security.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/public")
public class PublicController {
@GetMapping("/info")
public ResponseEntity<?> getPublicInfo() {
Map<String, String> info = new HashMap<>();
info.put("message", "This is public information");
info.put("access", "No authentication required");
return ResponseEntity.ok(info);
}
@GetMapping("/health")
public ResponseEntity<?> healthCheck() {
Map<String, String> health = new HashMap<>();
health.put("status", "UP");
health.put("service", "JWT Authentication Service");
return ResponseEntity.ok(health);
}
}
Application Configuration
Application Properties
# application.properties # Server Configuration server.port=8080 # JWT Configuration jwt.secret=mySecretKeyWhichIsVeryLongAndSecureForJWTTokenGeneration12345 jwt.expiration=86400 # 24 hours in seconds # Database Configuration (H2 for example) spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= # JPA Configuration spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true # H2 Console (for development) spring.h2.console.enabled=true
Data Initializer (Optional)
package com.example.security.config;
import com.example.security.model.User;
import com.example.security.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class DataInitializer implements CommandLineRunner {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void run(String... args) throws Exception {
// Create admin user
if (!userRepository.existsByUsername("admin")) {
User admin = new User(
"admin",
passwordEncoder.encode("admin123"),
"[email protected]",
"ADMIN"
);
userRepository.save(admin);
System.out.println("Admin user created");
}
// Create regular user
if (!userRepository.existsByUsername("user")) {
User user = new User(
"user",
passwordEncoder.encode("user123"),
"[email protected]",
"USER"
);
userRepository.save(user);
System.out.println("Regular user created");
}
}
}
Testing the JWT Authentication
Testing with curl Commands
# Test public endpoint (no authentication required)
curl http://localhost:8080/api/public/health
# Register a new user
curl -X POST http://localhost:8080/api/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"testuser", "password":"test123", "email":"[email protected]"}'
# Login and get JWT token
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"testuser", "password":"test123"}'
# Use the token to access protected user endpoint
curl http://localhost:8080/api/user/profile \
-H "Authorization: Bearer YOUR_JWT_TOKEN_HERE"
# Try to access admin endpoint with user token (should fail)
curl http://localhost:8080/api/admin/dashboard \
-H "Authorization: Bearer YOUR_JWT_TOKEN_HERE"
# Refresh token
curl -X POST http://localhost:8080/api/auth/refresh \
-H "Authorization: Bearer YOUR_JWT_TOKEN_HERE"
Integration Tests
package com.example.security.test;
import com.example.security.dto.AuthenticationRequest;
import com.example.security.dto.RegisterRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
public class JwtAuthenticationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
public void testPublicEndpoint() throws Exception {
mockMvc.perform(get("/api/public/health"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("UP"));
}
@Test
public void testRegisterAndLogin() throws Exception {
// Register new user
RegisterRequest registerRequest = new RegisterRequest(
"testuser", "password123", "[email protected]"
);
mockMvc.perform(post("/api/auth/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(registerRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("User registered successfully"));
// Login with registered user
AuthenticationRequest authRequest = new AuthenticationRequest(
"testuser", "password123"
);
mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(authRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.token").exists())
.andExpect(jsonPath("$.username").value("testuser"));
}
@Test
public void testProtectedEndpointWithoutToken() throws Exception {
mockMvc.perform(get("/api/user/profile"))
.andExpect(status().isUnauthorized());
}
}
Advanced Features
Custom JWT Claims
// Enhanced JWT Utility with custom claims
public String generateTokenWithClaims(UserDetails userDetails) {
User user = userRepository.findByUsername(userDetails.getUsername())
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("email", user.getEmail());
claims.put("role", user.getRole());
claims.put("createdAt", System.currentTimeMillis());
return doGenerateToken(claims, userDetails.getUsername());
}
public Long getUserIdFromToken(String token) {
return getClaimFromToken(token, claims -> claims.get("userId", Long.class));
}
public String getEmailFromToken(String token) {
return getClaimFromToken(token, claims -> claims.get("email", String.class));
}
Token Blacklisting
@Component
public class TokenBlacklistService {
private final Set<String> blacklistedTokens = ConcurrentHashMap.newKeySet();
public void blacklistToken(String token) {
blacklistedTokens.add(token);
}
public boolean isTokenBlacklisted(String token) {
return blacklistedTokens.contains(token);
}
// Clean up expired tokens periodically
@Scheduled(fixedRate = 3600000) // Run every hour
public void cleanupExpiredTokens() {
// Implementation to remove expired tokens from blacklist
}
}
Security Best Practices
- Use strong secret keys and rotate them regularly
- Set appropriate token expiration times
- Use HTTPS in production
- Implement proper password hashing (BCrypt)
- Add rate limiting for authentication endpoints
- Use secure cookie settings if storing tokens in cookies
- Implement proper CORS configuration
- Add logging and monitoring for security events
- Consider using refresh token rotation
- Implement proper error handling without information leakage
This comprehensive JWT authentication implementation provides a solid foundation for securing your Spring Boot applications with stateless, scalable authentication using JSON Web Tokens.