Spring Security | JWT

JWT Refresh Token : Spring Security

Zeeshan Adil
JavaToDev
Published in
8 min readOct 31, 2023

--

In the previous post, we learned how to create Token-based Authentication and Authorization using Spring Security and JWT. In this tutorial, we will extend our implementation to include JWT Refresh Tokens in a Java Spring Boot application. We’ll explore how to handle token expiration and renew access tokens using a refresh token.

Our specific requirement is that even if a token has expired, users should still be allowed to access the system if the token is valid. In other words, we aim to refresh the token or provide a new valid token when the user’s token has expired.

To achieve this, we will develop a solution where if a user encounters a JWT expired exception, they can call another API with the expired token. In response, a new token will be provided to the user, which they can use for future interactions with the system.

The sequence of steps for implementing JWT refresh token in a Spring Boot application is as follows in below diagram:

- When a client accesses protected resources, they must include a valid JWT in the HTTP Authorization Header.

  • Upon user sign-in, a refreshToken will be issued.

Let’s start coding… :)

Create a Model RefreshToken :

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class RefreshToken {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String token;
private Instant expiryDate;
@OneToOne
@JoinColumn(name = "user_id", referencedColumnName = "id")
private UserInfo userInfo;
}
  • private int id: This field represents the unique identifier for each refresh token.
  • private String token: This field stores some random string (UUID), which is a long-lived token used to obtain new access tokens.
  • private Instant expiryDate: This field represents the expiration date and time of the refresh token. It indicates when the refresh token will no longer be valid.
  • private UserInfo userInfo: This field represents the user associated with the refresh token. It establishes a one-to-one relationship between a user and their refresh token.

In summary, the RefreshToken class is a JPA entity used for managing refresh tokens in a database. Refresh tokens are issued to users during authentication and are used to obtain new access tokens without requiring the user to log in again. This entity class stores information about the token, its expiration date, and its association with a specific user through a one-to-one relationship.

Create RefreshToken repository:

@Repository
public interface RefreshTokenRepository extends CrudRepository<RefreshToken, Integer> {
Optional<RefreshToken> findByToken(String token);
}

Create RefreshTokenService class:

@Service
public class RefreshTokenService {

@Autowired
RefreshTokenRepository refreshTokenRepository;

@Autowired
UserRepository userRepository;

public RefreshToken createRefreshToken(String username){
RefreshToken refreshToken = RefreshToken.builder()
.userInfo(userRepository.findByUsername(username))
.token(UUID.randomUUID().toString())
.expiryDate(Instant.now().plusMillis(600000)) // set expiry of refresh token to 10 minutes - you can configure it application.properties file
.build();
return refreshTokenRepository.save(refreshToken);
}



public Optional<RefreshToken> findByToken(String token){
return refreshTokenRepository.findByToken(token);
}

public RefreshToken verifyExpiration(RefreshToken token){
if(token.getExpiryDate().compareTo(Instant.now())<0){
refreshTokenRepository.delete(token);
throw new RuntimeException(token.getToken() + " Refresh token is expired. Please make a new login..!");
}
return token;
}

}

createRefreshToken Method:

  • This method is used to create a new refresh token for a user.
  • It takes the username as input, fetches the associated user information from the UserRepository, generates a random token using UUID.randomUUID().toString(), and sets the expiration date to 10 minutes (600,000 milliseconds) from the current time.
  • It then builds a RefreshToken object and saves it to the database using the refreshTokenRepository.save(refreshToken) method. The newly created refresh token is returned.

findByToken Method:

  • This method is used to find a refresh token by its token value.
  • It takes a token as input and queries the database using the refreshTokenRepository.findByToken(token) method. The result is returned as an Optional to handle the possibility of a token not being found.

verifyExpiration Method:

  • This method checks if a refresh token has expired.
  • It takes a token as input and compares its expiration date with the current time (obtained through Instant.now()). If the expiration date is in the past, it means the token has expired.
  • If the token is expired, it is deleted from the database using refreshTokenRepository.delete(token), and a RuntimeException is thrown to indicate that the refresh token has expired.
  • If the token is still valid, it is returned as-is.

In summary, the RefreshTokenService class provides methods to create, find, and verify the expiration of refresh tokens.

Create AuthRequestDTO , JwtResponseDTO and RefreshTokenRequestDTO classes:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AuthRequestDTO {

private String username;
private String password;
}

--------------------------

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class JwtResponseDTO {

private String accessToken;
private String token;
}

---------------------------
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RefreshTokenRequestDTO {
private String token;
}

Create JwtService:

@Component
public class JwtService {

public static final String SECRET = "357638792F423F4428472B4B6250655368566D597133743677397A2443264629";

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
.parserBuilder()
.setSigningKey(getSignKey())
.build()
.parseClaimsJws(token)
.getBody();
}

private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}

public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}



public String GenerateToken(String username){
Map<String, Object> claims = new HashMap<>();
return createToken(claims, username);
}



private String createToken(Map<String, Object> claims, String username) {

return Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis()+1000*60*1))
.signWith(getSignKey(), SignatureAlgorithm.HS256).compact();
}

private Key getSignKey() {
byte[] keyBytes = Decoders.BASE64.decode(SECRET);
return Keys.hmacShaKeyFor(keyBytes);
}
}

The `JwtService` class is a component responsible for various operations related to JWT (JSON Web Tokens) in a Spring Boot application. It contains methods for generating, parsing, and validating JWT tokens. Please follow my previous post for detailed explanation of this service.

Create login API (/api/v1/login):

@PostMapping("/login")
public JwtResponseDTO AuthenticateAndGetToken(@RequestBody AuthRequestDTO authRequestDTO){
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(authRequestDTO.getUsername(), authRequestDTO.getPassword()));
if(authentication.isAuthenticated()){
RefreshToken refreshToken = refreshTokenService.createRefreshToken(authRequestDTO.getUsername());
return JwtResponseDTO.builder()
.accessToken(jwtService.GenerateToken(authRequestDTO.getUsername()))
.token(refreshToken.getToken())
.build();

} else {
throw new UsernameNotFoundException("invalid user request..!!");
}
}

Authentication:

  • Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(authRequestDTO.getUsername(), authRequestDTO.getPassword())): This line attempts to authenticate the user by using the authenticationManager. It creates an UsernamePasswordAuthenticationToken with the provided username and password and passes it to the authenticationManager. If the authentication is successful, it means the user's credentials are valid.

Token Generation and Refresh Token Creation:

  • If the authentication is successful (i.e., the user’s credentials are valid), the code proceeds to create a new refresh token and generate JWT tokens.
  • RefreshToken refreshToken = refreshTokenService.createRefreshToken(authRequestDTO.getUsername()): It calls the refreshTokenService.createRefreshToken method, passing the username from the AuthRequestDTO. This method generates a new refresh token and associates it with the user.

Response:

  • The method returns the JwtResponseDTO, which includes the JWT access token and the refresh token. The client can use the access token for authenticated API requests and store the refresh token for obtaining new access tokens when the current one expires.

Exception Handling:

  • If authentication fails (i.e., the provided credentials are invalid), the code throws a UsernameNotFoundException with the message "invalid user request..!!".

Create refresh token API (/api/v1/refreshToken):

@PostMapping("/refreshToken")
public JwtResponseDTO refreshToken(@RequestBody RefreshTokenRequestDTO refreshTokenRequestDTO){
return refreshTokenService.findByToken(refreshTokenRequestDTO.getToken())
.map(refreshTokenService::verifyExpiration)
.map(RefreshToken::getUserInfo)
.map(userInfo -> {
String accessToken = jwtService.GenerateToken(userInfo.getUsername());
return JwtResponseDTO.builder()
.accessToken(accessToken)
.token(refreshTokenRequestDTO.getToken()).build();
}).orElseThrow(() ->new RuntimeException("Refresh Token is not in DB..!!"));
}

Token Refresh Process:

  • The method begins by attempting to refresh the access token using the provided refresh token(uuid for refresh token generated for the user who logged in).
  • refreshTokenService.findByToken(refreshTokenRequestDTO.getToken()): It calls the refreshTokenService to find a refresh token in the database using the token provided in the request.
  • .map(refreshTokenService::verifyExpiration): If a refresh token is found, the code then applies the verifyExpiration method from the refreshTokenService to ensure that the refresh token has not expired. This step is crucial for security.
  • .map(RefreshToken::getUserInfo): If the refresh token is valid and not expired, it maps the refresh token to the associated UserInfo, which likely contains the user's information.
  • Inside the final .map(userInfo -> { ... }) block, the code generates a new access token based on the user's information:
  • String accessToken = jwtService.GenerateToken(userInfo.getUsername()): It uses the jwtService to generate a new JWT access token based on the username of the user associated with the refresh token.
  • Finally, it constructs a JwtResponseDTO object that includes the newly generated access token and the same refresh token provided in the request. This response object is then returned.

Bypass the login (/api/v1/login) & refreshToken (/api/v1/refreshToken) endpoints from security:

Now we have to bypass login, refreshToken APIs endpoints from securityConfig. These apis must not be authenticated so that user must have flexibility to generate the token and refresh token.

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.csrf().disable()
.authorizeHttpRequests()
.requestMatchers( "/api/v1/login", "/api/v1/refreshToken").permitAll()
.and()
.authorizeHttpRequests().requestMatchers("/api/v1/**")
.authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class).build();

}

We have almost done with coding…. :) It’s time to test our implementation.

I’ve manually added a user with the username “username123” and the password “test2day” in the system. I didn’t create an API endpoint for user registration because my primary focus was on implementing refresh Token functionality. Instead, I directly inserted the user into the database.

Upon successful login, we are getting accessToken and token (which is nothing but a random uuid for username username123). We can see in our db. This uuid can be used to generate the refresh token.

Pass this uuid in request to obtain the new accessToken (Refresh Token).

✅Find the source code here…

Next to Learn 👇

Invalidate/Revoked the JWT : Force logout the user from spring security

Cookie-based JWT Authentication with Spring Security

From Localhost to the Cloud: Deploying Spring Boot + MySQL App on Kubernetes with Docker Desktop — A Beginner Guide

❤️ Support & Engagement ❤️

❤ If you find this article informative and beneficial, please consider showing your appreciation by giving it a clap 👏👏👏, highlight it and replying on my story story. Feel free to share this article with your peers. Your support and knowledge sharing within the developer community are highly valued.
Please share on social media
Follow me on : Medium || LinkedIn
Check out my other articles on Medium
Subscribe to my newsletter 📧, so that you don’t miss out on my latest articles.
❤ If you enjoyed my article, please consider buying me a coffee ❤️ and stay tuned to more articles about java, technologies and AI. 🧑‍💻

--

--

Zeeshan Adil
JavaToDev

Full Stack Developer || Educator || Technical Blogger 🧑‍💻Let's Connect : https://www.linkedin.com/in/zeeshan-adil-a94b3867/