JSON Web Tokens (JWT)
A JWT consists of 3 parts*
1.Header: Contains metadata of token > Type (JWT), signing algorithm.
{
"alg": "HS256", #Algorithem for siging
"typ": "JWT" # Token type
}
2.Payload: Contains the claims and other data related to user.
{
"userId": 1,
"role": "admin",
"exp": 840000
}
3.Signature: Verify JWT sender, immutable, ensures token integrity and is generated with header, payload, and a secret key.
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
JWT Authentication Flow
- Login Request: The user sends their credentials (Login request) to the server (username and password).
- Server Generates JWT: Server validates user's credentials, if they are valid, server generates a JWT and sends it back to user.
- Return JWT: A user stores the JWT in local storage or a cookie on browser.
- Further Requests: A user requests subsequentially, JWT will be sent in the Authorization header.
- Server verification: verifies the JWT and grants access to authentication required protected data.
JWT Hand-On Understanding
Lab: JWT authentication bypass via unverified signature | Web Security Academy
Security Considerations
- Use HTTPS: encrypts information and prevents unauthorized access.
- Set Short Expiration Time
- Use Refresh Tokens
- Rotate Secrets
- Use Secure Storage
- Verify Signature
Implementation with Spring Boot 3
Basics required dependencies: Spring Web, Spring Security, Spring Data JPA, SQL driver, Spring Boot DevTools
for Maven
spring-boot-starter-web spring-boot-starter-security spring-boot-starter-data-jpa mysql-connector-java spring-boot-starter-devtools jjwt
1.Create User model
package com.example.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String role;
// Getters and Setters
}
2.Implement BCrypt
Bcrypt is a password hashing function
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3.Create the JWT Util Class
package com.example.security.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
/**
* Utility class for generating and validating JWT tokens.
*/
@Component
public class JwtUtil {
// Secret key for signing the JWT (should be stored securely, e.g., in environment variables)
private final String SECRET_KEY = "your_secret_key_here";
// Token validity period (e.g., 10 hours)
private final long JWT_EXPIRATION_MS = 10 * 60 * 60 * 1000;
/**
* Generate a JWT token for a given username.
*
* @param username the username to include in the token
* @return signed JWT token
*/
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, username);
}
/**
* Create a JWT token with claims and subject.
*/
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims) // custom claims
.setSubject(subject) // username
.setIssuedAt(new Date(System.currentTimeMillis())) // issue time
.setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION_MS)) // expiry time
.signWith(SignatureAlgorithm.HS256, SECRET_KEY) // sign with secret key
.compact();
}
/**
* Extract username from token.
*/
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
/**
* Extract expiration date from token.
*/
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
/**
* Extract a specific claim from token.
*/
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
/**
* Parse all claims from token.
*/
private Claims extractAllClaims(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
/**
* Check if token is expired.
*/
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
/**
* Validate token against username and expiration.
*/
public boolean validateToken(String token, String username) {
try {
final String extractedUsername = extractUsername(token);
return (extractedUsername.equals(username) && !isTokenExpired(token));
} catch (ExpiredJwtException | MalformedJwtException |
SignatureException | UnsupportedJwtException |
IllegalArgumentException e) {
// Log the exception in real applications
return false;
}
}
}
4.User Details Service
Service that loads user-specific data for authentication, fetching user detals from the database.
import com.basicutils.jwtauthexample.model.User;
import com.basicutils.jwtauthexample.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
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;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), new ArrayList<>());
}
}
5.Create the JWT Request Filter
The Filter will intercept request and validate JWT Token before allowing access to protected routes.
package com.example.filter;
import com.basicutils.jwtauthexample.service.UserDetailsServiceImpl;
import com.basicutils.jwtauthexample.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
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 JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@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.getUsername())) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
chain.doFilter(request, response);
}
}
5.Security Configuration
Set up the security configuration using Spring Security
package com.example.config;
import com.basicutils.jwtauthexample.filter.JwtRequestFilter;
import com.basicutils.jwtauthexample.service.UserDetailsServiceImpl;
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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
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.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder =
http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
return authenticationManagerBuilder.build();
}
}
References
- geeksforgeeks - JSON Web Token (JWT)
- basicutils - Implementing JWT Authentication with Spring Boot 3