Spring Security Refresh Token with JWT in Spring Boot

In previous post, we’ve known how to build Token based Authentication & Authorization with Spring Security & JWT. This tutorial will continue to make JWT Refresh Token with Spring Security in the Java Spring Boot Application. You can know how to expire the JWT Token, then renew the Access Token with Refresh Token and HttpOnly Cookie.

Related Posts:

Deployment:
Deploy Spring Boot App on AWS – Elastic Beanstalk
Docker Compose: Spring Boot and MySQL example

The code in this post bases on previous article that you need to read first:
Spring Boot JWT Auth example with H2 Database
Spring Boot JWT Auth example with MySQL
Spring Boot JWT Auth example with MongoDB


Overview

Spring Boot Security JWT Authentication & Authorization

We already have a Spring Boot – Spring Security application in that:

  • User can signup new account, or login with username & password.
  • By User’s role (admin, moderator, user), we authorize the User to access resources
  • The access Token is stored in HttpOnly Cookie

With APIs:

Methods Urls Actions
POST /api/auth/signup signup new account
POST /api/auth/signin login an account
POST /api/auth/signout logout the account
GET /api/test/all retrieve public content
GET /api/test/user access User’s content
GET /api/test/mod access Moderator’s content
GET /api/test/admin access Admin’s content

For more details, please visit one of following posts:
Spring Boot JWT Auth example with H2 Database
Spring Boot JWT Auth example with MySQL
Spring Boot JWT Auth example with MongoDB

Spring Security Refresh Token endpoint

We will export new endpoint for token refresh:

Methods Urls Actions
POST /api/auth/refreshtoken generate new Access Token

Spring Security Refresh Token with JWT example

Here are some requests to the endpoints that our Spring Boot Security JWT Refresh Token example exports.

– Send /signin request.

spring-security-refresh-token-jwt-spring-boot-signin

access Token & refresh Token are stored in the HttpOnly Cookies:

spring-security-refresh-token-jwt-spring-boot-httponly-cookie

– Access resource successfully with access Token (in HttpOnly Cookie).

spring-security-refresh-token-jwt-spring-boot-access

– When the access Token is expired, user cannot use it anymore.

spring-security-refresh-token-jwt-spring-boot-unauthorized

– Send /refreshtoken request.

spring-security-refresh-token-jwt-spring-boot-endpoint

Server returns response with new access Token in HttpOnly Cookie:

spring-security-refresh-token-jwt-spring-boot-new-token

– Access resource successfully with new access Token.

spring-security-refresh-token-jwt-spring-boot-authorization

– When refresh Token is expired (in HttpOnly Cookie).

spring-security-refresh-token-jwt-spring-boot-token-expired

– If the refresh Token is expired, it will be removed from database and become inexistent. Let’s send an inexistent Refresh Token (in HttpOnly Cookie).

spring-security-refresh-token-jwt-spring-boot-token-deleted

Flow for Refresh Token with JWT

The diagram shows flow of how we implement Authentication & Authorization process with Access Token and Refresh Token.

spring-security-refresh-token-jwt-spring-boot-flow

– A refresh Token will be provided in HttpOnly Cookie at the time user signs in successfully.
– A legal JWT must be stored in HttpOnly Cookie if Client accesses protected resources.

How to Expire JWT Token in Spring Boot

The Refresh Token has different value and expiration time to the Access Token.
Regularly we configure the expiration time of Refresh Token larger than Access Token’s.

Open application.properties for configuring App properties:

# Spring Datasource, Spring Data...

# App Properties
bezkoder.app.jwtCookieName= bezkoder-jwt
bezkoder.app.jwtRefreshCookieName= bezkoder-jwt-refresh
bezkoder.app.jwtSecret= bezKoderSecretKey
bezkoder.app.jwtExpirationMs= 86400000
bezkoder.app.jwtRefreshExpirationMs= 86400000

## For test
#bezkoder.app.jwtExpirationMs= 60000
#bezkoder.app.jwtRefreshExpirationMs= 120000

Update JwtUtils class. Now it has some necessary functions:

  • generate JWT, refresh Token
  • get JWT, refresh Token from Cookies
  • validate a JWT: JWT Access Token is expired with ExpiredJwtException

security/jwt/JwtUtils.java

package com.bezkoder.spring.security.jwt.security.jwt;

import java.util.Date;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;
import org.springframework.web.util.WebUtils;

import com.bezkoder.spring.security.jwt.models.User;
import com.bezkoder.spring.security.jwt.security.services.UserDetailsImpl;

import io.jsonwebtoken.*;

@Component
public class JwtUtils {
  private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);

  @Value("${bezkoder.app.jwtSecret}")
  private String jwtSecret;

  @Value("${bezkoder.app.jwtExpirationMs}")
  private int jwtExpirationMs;

  @Value("${bezkoder.app.jwtCookieName}")
  private String jwtCookie;
  
  @Value("${bezkoder.app.jwtRefreshCookieName}")
  private String jwtRefreshCookie;

  public ResponseCookie generateJwtCookie(UserDetailsImpl userPrincipal) {
    String jwt = generateTokenFromUsername(userPrincipal.getUsername());   
    return generateCookie(jwtCookie, jwt, "/api");
  }
  
  public ResponseCookie generateJwtCookie(User user) {
    String jwt = generateTokenFromUsername(user.getUsername());   
    return generateCookie(jwtCookie, jwt, "/api");
  }
  
  public ResponseCookie generateRefreshJwtCookie(String refreshToken) {
    return generateCookie(jwtRefreshCookie, refreshToken, "/api/auth/refreshtoken");
  }
  
  public String getJwtFromCookies(HttpServletRequest request) {
    return getCookieValueByName(request, jwtCookie);
  }
  
  public String getJwtRefreshFromCookies(HttpServletRequest request) {
    return getCookieValueByName(request, jwtRefreshCookie);
  }

  public ResponseCookie getCleanJwtCookie() {
    ResponseCookie cookie = ResponseCookie.from(jwtCookie, null).path("/api").build();
    return cookie;
  }
  
  public ResponseCookie getCleanJwtRefreshCookie() {
    ResponseCookie cookie = ResponseCookie.from(jwtRefreshCookie, null).path("/api/auth/refreshtoken").build();
    return cookie;
  }

  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;
  }
  
  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();
  }
    
  private ResponseCookie generateCookie(String name, String value, String path) {
    ResponseCookie cookie = ResponseCookie.from(name, value).path(path).maxAge(24 * 60 * 60).httpOnly(true).build();
    return cookie;
  }
  
  private String getCookieValueByName(HttpServletRequest request, String name) {
    Cookie cookie = WebUtils.getCookie(request, name);
    if (cookie != null) {
      return cookie.getValue();
    } else {
      return null;
    }
  }
}

Renew JWT Token in Spring Boot

In the AuthController class, we:

  • update the method for /signin, /signout endpoint with Refresh Token
  • expose /refreshtoken endpoint for creating new Access Token from Refresh Token

controllers/AuthController.java

@RestController
@RequestMapping("/api/auth")
public class AuthController {
  @Autowired
  AuthenticationManager authenticationManager;

  ...

  @Autowired
  JwtUtils jwtUtils;

  @Autowired
  RefreshTokenService refreshTokenService;

  @PostMapping("/signin")
  public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {

    Authentication authentication = authenticationManager
        .authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));

    SecurityContextHolder.getContext().setAuthentication(authentication);

    UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();

    ResponseCookie jwtCookie = jwtUtils.generateJwtCookie(userDetails);

    List<String> roles = userDetails.getAuthorities().stream()
        .map(item -> item.getAuthority())
        .collect(Collectors.toList());
    
    RefreshToken refreshToken = refreshTokenService.createRefreshToken(userDetails.getId());
    
    ResponseCookie jwtRefreshCookie = jwtUtils.generateRefreshJwtCookie(refreshToken.getToken());

    return ResponseEntity.ok()
              .header(HttpHeaders.SET_COOKIE, jwtCookie.toString())
              .header(HttpHeaders.SET_COOKIE, jwtRefreshCookie.toString())
              .body(new UserInfoResponse(userDetails.getId(),
                                         userDetails.getUsername(),
                                         userDetails.getEmail(),
                                         roles));
  }

  @PostMapping("/signup")
  public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signUpRequest) {
    ...
  }

  @PostMapping("/signout")
  public ResponseEntity<?> logoutUser() {
    Object principle = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    if (principle.toString() != "anonymousUser") {      
      Long userId = ((UserDetailsImpl) principle).getId();
      refreshTokenService.deleteByUserId(userId);
    }
    
    ResponseCookie jwtCookie = jwtUtils.getCleanJwtCookie();
    ResponseCookie jwtRefreshCookie = jwtUtils.getCleanJwtRefreshCookie();

    return ResponseEntity.ok()
        .header(HttpHeaders.SET_COOKIE, jwtCookie.toString())
        .header(HttpHeaders.SET_COOKIE, jwtRefreshCookie.toString())
        .body(new MessageResponse("You've been signed out!"));
  }

  @PostMapping("/refreshtoken")
  public ResponseEntity<?> refreshtoken(HttpServletRequest request) {
    String refreshToken = jwtUtils.getJwtRefreshFromCookies(request);
    
    if ((refreshToken != null) && (refreshToken.length() > 0)) {
      return refreshTokenService.findByToken(refreshToken)
          .map(refreshTokenService::verifyExpiration)
          .map(RefreshToken::getUser)
          .map(user -> {
            ResponseCookie jwtCookie = jwtUtils.generateJwtCookie(user);
            
            return ResponseEntity.ok()
                .header(HttpHeaders.SET_COOKIE, jwtCookie.toString())
                .header(HttpHeaders.SET_COOKIE, refreshToken)
                .body(new MessageResponse("Token is refreshed successfully!"));
          })
          .orElseThrow(() -> new TokenRefreshException(refreshToken,
              "Refresh token is not in database!"));
    }
    
    return ResponseEntity.badRequest().body(new MessageResponse("Refresh Token is empty!"));
  }
}

In signout() method:

  • We get Principal object to check if the current user is authenticated or not.
  • Continue to use user’s id field to remove Refresh Token from database using RefreshTokenService
  • Then we clear the appropriate HttpOnly Cookies
  • Return response with clean HttpOnly Cookies

In refreshtoken() method:

  • Firstly, we get the Refresh Token from request data
  • Next, get the RefreshToken object {id, user, token, expiryDate} from raw Token using RefreshTokenService
  • We verify the token (expired or not) basing on expiryDate field
  • Continue to use user field of RefreshToken object as parameter to generate new Access Token using JwtUtils
  • Return response with new Access Token in HttpOnly Cookie if everything is done
  • Or else, throw TokenRefreshException

Create Refresh Token Service

Refresh Token class

This class has one-to-one relationship with User class.

models/RefreshToken.java

package com.bezkoder.spring.security.jwt.models;

import java.time.Instant;

import javax.persistence.*;

@Entity(name = "refreshtoken")
public class RefreshToken {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;

  @OneToOne
  @JoinColumn(name = "user_id", referencedColumnName = "id")
  private User user;

  @Column(nullable = false, unique = true)
  private String token;

  @Column(nullable = false)
  private Instant expiryDate;

  // getters and setters
}

Refresh Token Repository

Before creating the service, we need RefreshTokenRepository with finder methods:

repository/RefreshTokenRepository.java

package com.bezkoder.spring.security.jwt.repository;
import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.stereotype.Repository;

import com.bezkoder.spring.security.jwt.models.RefreshToken;
import com.bezkoder.spring.security.jwt.models.User;

@Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
  Optional<RefreshToken> findByToken(String token);

  @Modifying
  int deleteByUser(User user);
}

Refresh Token Service

After that, we have the RefreshTokenService service which uses RefreshTokenRepository above for providing several useful methods:

  • findByToken(): Find a RefreshToken based on the natural id i.e the token itself
  • createRefreshToken(): Create and return a new Refresh Token
  • verifyExpiration(): Verify whether the token provided has expired or not. If the token was expired, delete it from database and throw TokenRefreshException

security/services/RefreshTokenRepository.java

package com.bezkoder.spring.security.jwt.security.services;

import java.time.Instant;
import java.util.Optional;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.bezkoder.spring.security.jwt.exception.TokenRefreshException;
import com.bezkoder.spring.security.jwt.models.RefreshToken;
import com.bezkoder.spring.security.jwt.repository.RefreshTokenRepository;
import com.bezkoder.spring.security.jwt.repository.UserRepository;

@Service
public class RefreshTokenService {
  @Value("${bezkoder.app.jwtRefreshExpirationMs}")
  private Long refreshTokenDurationMs;

  @Autowired
  private RefreshTokenRepository refreshTokenRepository;

  @Autowired
  private UserRepository userRepository;

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

  public RefreshToken createRefreshToken(Long userId) {
    RefreshToken refreshToken = new RefreshToken();

    refreshToken.setUser(userRepository.findById(userId).get());
    refreshToken.setExpiryDate(Instant.now().plusMillis(refreshTokenDurationMs));
    refreshToken.setToken(UUID.randomUUID().toString());

    refreshToken = refreshTokenRepository.save(refreshToken);
    return refreshToken;
  }

  public RefreshToken verifyExpiration(RefreshToken token) {
    if (token.getExpiryDate().compareTo(Instant.now()) < 0) {
      refreshTokenRepository.delete(token);
      throw new TokenRefreshException(token.getToken(), "Refresh token was expired. Please make a new signin request");
    }

    return token;
  }

  @Transactional
  public int deleteByUserId(Long userId) {
    return refreshTokenRepository.deleteByUser(userRepository.findById(userId).get());
  }
}

Handle Token Refresh Exception

Now we need to create TokenRefreshException class that extends RuntimeException.

exception/TokenRefreshException.java

package com.bezkoder.spring.security.jwt.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.FORBIDDEN)
public class TokenRefreshException extends RuntimeException {

  private static final long serialVersionUID = 1L;

  public TokenRefreshException(String token, String message) {
    super(String.format("Failed for [%s]: %s", token, message));
  }
}

Let’s do the final step. We’re gonna create a RestControllerAdvice.

advice/TokenRefreshException.java

package com.bezkoder.spring.security.jwt.advice;

import java.util.Date;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;

import com.bezkoder.spring.security.jwt.exception.TokenRefreshException;

@RestControllerAdvice
public class TokenControllerAdvice {

  @ExceptionHandler(value = TokenRefreshException.class)
  @ResponseStatus(HttpStatus.FORBIDDEN)
  public ErrorMessage handleTokenRefreshException(TokenRefreshException ex, WebRequest request) {
    return new ErrorMessage(
        HttpStatus.FORBIDDEN.value(),
        new Date(),
        ex.getMessage(),
        request.getDescription(false));
  }
}

For more details about RestControllerAdvice, please visit:
@RestControllerAdvice example in Spring Boot

Conclusion

Congratulation!

Today we’ve learned a more interesting thing about JWT Refresh Token in a Spring Boot & Spring Security example.
Despite we wrote a lot of code, I hope you will understand the overall idea of the application, and apply it in your project at ease.

The code in this post bases on previous article that you need to read first:

For understanding the architecture deeply and grasp the overview more easier:
Spring Boot Architecture for JWT with Spring Security

You can also know how to deploy Spring Boot App on AWS (for free) with this tutorial.
Or Dockerize with :Docker Compose: Spring Boot and MySQL example

Happy learning! See you again.

Further Reading

More Practices:

Fullstack CRUD App:
Spring Boot + Vue.js example
Angular 8 + Spring Boot example
Angular 10 + Spring Boot example
Angular 11 + Spring Boot example
Angular 12 + Spring Boot example
Angular 13 + Spring Boot example
Angular 14 + Spring Boot example
React + Spring Boot example

Associations:
JPA One To One example with Hibernate in Spring Boot
JPA One To Many example with Hibernate and Spring Boot
JPA Many to Many example with Hibernate in Spring Boot

Source Code

You can find the complete source code for this tutorial on Github.