In modern application development, stateless authentication using JWT (JSON Web Tokens) has become the de facto standard for securing REST APIs. This blog provides a complete walkthrough of implementing JWT token-based authentication in a Spring Boot application.
What is JWT?
JWT (JSON Web Token) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.
It is widely used for authorization, especially in stateless microservices architecture where session-based mechanisms are not ideal.
Structure of a JWT
A JWT consists of three parts:
- Header: Contains metadata about the type of token and algorithm used.
- Payload: Contains the claims or user data (e.g., username, roles).
- Signature: Used to verify the integrity of the token using a secret key or public/private key pair.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNjEyODgzMjAwfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Why Use JWT for Authentication?
Compared to traditional session-based authentication, JWT offers several advantages:
- Stateless: No session storage needed on the server.
- Compact: Transmitted via HTTP headers or query parameters.
- Secure: Digitally signed to ensure integrity.
- Scalable: Easy to implement in distributed systems and microservices.
- Cross-domain: Works across different domains and platforms.
How JWT Works
The lifecycle of JWT-based authentication in a Spring Boot application is:
- Client sends login credentials to the authentication endpoint.
- Server verifies credentials and generates a signed JWT.
- Server returns the JWT to the client in the response body or header.
- Client includes the JWT in the Authorization header of each subsequent request.
- Server validates the token, extracts user data, and allows or denies access.
Authorization Header Format:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWT vs Session-Based Authentication
Feature | Session-Based | JWT-Based |
---|---|---|
Storage | Stored on server | Stored on client (token) |
Scalability | Less scalable (server memory required) | Highly scalable (stateless) |
Security | Susceptible to CSRF | Secure with HMAC or RSA signature |
Cross-domain | Limited | Flexible |
Prerequisites for JWT in Spring Boot
Before implementing JWT authentication, make sure you have the following:
- Java 8+
- Spring Boot 2.6 or above
- Dependencies:
spring-boot-starter-security
,spring-boot-starter-web
, andjjwt
orjava-jwt
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
Creating the JWT Utility Class
The JwtUtil
class is responsible for:
- Generating a JWT token
- Validating a JWT token
- Extracting username or claims from token
JwtUtil.java
@Component
public class JwtUtil {
private final String SECRET_KEY = "your-secret-key";
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <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.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
Authentication Request & Response Models
You’ll need two simple models to handle authentication requests and responses.
AuthenticationRequest.java
public class AuthenticationRequest {
private String username;
private String password;
// Constructors, getters, setters
}
AuthenticationResponse.java
public class AuthenticationResponse {
private String jwt;
public AuthenticationResponse(String jwt) {
this.jwt = jwt;
}
public String getJwt() {
return jwt;
}
}
Creating the Authentication Controller
This REST controller will expose the /authenticate
endpoint that handles login requests and returns a JWT token.
@RestController
public class AuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
@PostMapping("/authenticate")
public ResponseEntity<?> createAuthenticationToken(
@RequestBody AuthenticationRequest authRequest) throws Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
authRequest.getUsername(), authRequest.getPassword()));
} catch (BadCredentialsException e) {
throw new Exception("Incorrect username or password", e);
}
final UserDetails userDetails =
userDetailsService.loadUserByUsername(authRequest.getUsername());
final String jwt = jwtUtil.generateToken(userDetails);
return ResponseEntity.ok(new AuthenticationResponse(jwt));
}
}
Configuring AuthenticationManager
Spring Boot does not auto-configure an AuthenticationManager
bean in newer versions. You must define it manually.
@Configuration
public class AppConfig {
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
}
Custom UserDetailsService
You can define your own UserDetailsService
to fetch user credentials from a database or in-memory.
@Service
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
return new User("admin", "{noop}password", new ArrayList<>());
}
}
Creating a JWT Request Filter
The JWT filter ensures every incoming HTTP request is inspected for a valid token. If valid, it sets the authentication in the Spring Security context.
JwtRequestFilter.java
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private MyUserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authHeader != null && authHeader.startsWith("Bearer ")) {
jwt = authHeader.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 authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
chain.doFilter(request, response);
}
}
Configuring SecurityFilterChain
In Spring Boot 2.7+ and Spring Security 5.7+, use a bean of type SecurityFilterChain
instead of extending WebSecurityConfigurerAdapter
.
SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests()
.requestMatchers("/authenticate").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
This setup ensures:
- CSRF protection is disabled for APIs
-
/authenticate
is publicly accessible - All other endpoints require a valid JWT
- Stateless session policy is enforced
Protecting REST Endpoints
Now that Spring Security is set up, any request to protected endpoints will go through the filter, validate the token, and establish the security context.
Example secure REST controller:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello from secured endpoint!";
}
}
When called without a token:
HTTP 403 Forbidden
When called with a valid JWT in the header:
Authorization: Bearer <your-jwt-token>
You will receive:
Hello from secured endpoint!
Testing Your JWT Setup
Use Postman, curl, or your frontend to:
- Send POST to
/authenticate
with JSON body:{ "username": "admin", "password": "password" }
- Copy the returned JWT from the response.
- Add it to the Authorization header for future requests:
- Send GET request to
/hello
and check the response.
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Handling Token Expiry
JWTs should have a defined expiration time to prevent misuse. This is configured during token creation.
Example (in JwtUtil.java):
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10 hours
On the client side, you should always track expiry. If the token is expired, your backend will return:
HTTP 403 Forbidden - Token Expired
To handle this, implement refresh tokens or force a re-login.
Implementing Refresh Tokens
A common pattern is to issue both:
- Access Token (short-lived JWT)
- Refresh Token (longer-lived, stored securely)
The refresh token is sent to a secure endpoint like /refresh
, validated, and a new JWT is issued.
Example endpoint:
@PostMapping("/refresh")
public ResponseEntity<?> refresh(@RequestBody RefreshRequest request) {
String refreshToken = request.getToken();
// Validate refresh token logic here
String newJwt = jwtUtil.generateTokenFromRefresh(refreshToken);
return ResponseEntity.ok(new AuthenticationResponse(newJwt));
}
Common Mistakes in JWT Implementation
- Using plain strings as secret keys (use long random secrets)
- Not setting an expiration time
- Not validating the token signature
- Allowing JWT in URLs (use headers only)
- Ignoring security for refresh token endpoints
Secure your application by following OWASP recommendations and rotating tokens periodically.
Best Practices
- Use
HS512
or RSA for signing - Store tokens in HTTP-only cookies or secure local storage
- Always use
Bearer <token>
format in headers - Set token lifetime wisely (15-30 mins typical)
- Enable refresh token flow for longer sessions
- Use HTTPS to avoid token leakage
Final Summary
JWT authentication in Spring Boot offers a modern, secure, and scalable way to handle user sessions without storing state. You’ve now learned:
- What JWT is and why it’s useful
- How to generate and validate JWT tokens
- How to protect endpoints with custom filters
- How to implement refresh tokens and handle expiry
With this knowledge, you can secure any REST API and integrate it with modern frontend frameworks like React, Angular, or mobile apps.
Pro Tip: Always keep security updates in check. JWT is powerful, but only when used correctly.
0 Comments