Introduction
JSON Web Token (JWT) is widely used for authentication and authorization. However, JWTs have an expiration time, meaning users must log in again when the token expires.
To improve user experience and security, we can implement a refresh token mechanism, which allows users to get a new access token without re-entering their credentials.
In this tutorial, we will implement a Spring Boot JWT authentication system with refresh tokens.
Project Setup
Step 1: Create a Spring Boot Project
- Go to Spring Initializr.
- Select the following dependencies:
- Spring Web (for REST APIs)
- Spring Security (for authentication)
- Spring Data JPA (for database interactions)
- MySql Database (for testing)
- jjwt (for JWT handling)
- Click Generate, download the project, and open it in your IDE.
Step 2: Configure Application Properties
Define JWT secret and expiration times in application.properties
spring.application.name=demo
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/demo
spring.datasource.username = root
spring.datasource.password = root
spring.jpa.hibernate.ddl-auto = update
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQLDialect
security.jwt.secret-key=3cfa76ef14937c1c0ea519f8fc057a80fcd04a7420f8e8bcd0a7567c272e007b
# 1h in millisecond
security.jwt.expiration-time=600000
spring.jackson.default-property-inclusion = NON_NULL
Step 3: Implement JWT Utility Class
This class is responsible for generating, validating, and extracting claims from JWT tokens.
- Create
JwtService.java
package com.example.demo.service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import com.example.demo.Dao.UserEntity;
@Service
public class JwtService {
@Value("${security.jwt.secret-key}")
private String secretKey;
@Value("${security.jwt.expiration-time}")
private long jwtExpiration;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return buildToken(extraClaims, userDetails, jwtExpiration);
}
public long getExpirationTime() {
return jwtExpiration;
}
private String buildToken(
Map<String, Object> extraClaims,
UserDetails userDetails,
long expiration
) {
return Jwts
.builder()
.setClaims(extraClaims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private Claims extractAllClaims(String token) {
return Jwts
.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
}
private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
public String GenerateToken(UserEntity userInfo) {
Map<String, Object> claims = new HashMap<>();
return this.buildToken(claims, userInfo, jwtExpiration);
}
}
Step 4: Create Refresh Token Entity
We need to store refresh tokens in the database.
- Create
RefreshToken.java
@Entity
@Setter
@Getter
@JsonSerialize(include=JsonSerialize.Inclusion.NON_EMPTY)
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 UserEntity userInfo;
}
- Create
RefreshTokenRepository.java
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.example.demo.Dao.RefreshToken;
import com.example.demo.Dao.UserEntity;
@Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Integer> {
Optional findByToken(String token);
RefreshToken findByUserInfo(UserEntity user);
}
Step 5: Implement Refresh Token Service
This service handles creating, validating, and deleting refresh tokens.
- Create
RefreshTokenService.java
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import com.example.demo.Dao.RefreshToken;
import com.example.demo.repository.RefreshTokenRepository;
import com.example.demo.repository.UserRepository;
@Service
public class RefreshTokenService {
@Autowired
RefreshTokenRepository refreshTokenRepository;
@Autowired
UserRepository userRepository;
public RefreshToken createRefreshToken(String username){
RefreshToken refreshToken = refreshTokenRepository.findByUserInfo(userRepository.findByEmail(username));
if(ObjectUtils.isEmpty(refreshToken)) {
// refreshTokenRepository.delete(refreshTokenRepository.findByUserInfo(userRepository.findByEmail(username)));
refreshToken = new RefreshToken();
}
refreshToken.setUserInfo(userRepository.findByEmail(username));
refreshToken.setToken(UUID.randomUUID().toString());
refreshToken.setExpiryDate(Instant.now().plusMillis(3600000)); // set expiry of refresh token to 10 minutes - you can configure it application.properties file
return refreshTokenRepository.save(refreshToken);
}
public Optional 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;
}
}
Step 6: Create Authentication Controller
This controller handles user login and refresh token requests.
- Create
UserController.java
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.Dao.RefreshToken;
import com.example.demo.Dao.UserEntity;
import com.example.demo.Dto.RefreshTokenRequestDTO;
import com.example.demo.Dto.UserDto;
import com.example.demo.Dto.UserResponse;
import com.example.demo.repository.UserRepository;
import com.example.demo.service.AuthenticationService;
import com.example.demo.service.JwtService;
import com.example.demo.service.RefreshTokenService;
import java.time.Instant;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@RestController
@RequestMapping("api/v1/")
public class UserController {
@Autowired
private JwtService jwtService;
@Autowired
private AuthenticationService authenticationService;
@Autowired
private RefreshTokenService refreshTokenService;
@Autowired
private UserRepository userRepository;
@PostMapping(value = "signup")
public ResponseEntity register(@RequestBody UserDto registerUserDto) {
UserEntity registeredUser = authenticationService.signup(registerUserDto);
return ResponseEntity.ok(registeredUser);
}
@PostMapping(value = "login")
public ResponseEntity authenticate(@RequestBody UserDto loginUserDto) {
UserEntity authenticatedUser = authenticationService.authenticate(loginUserDto);
RefreshToken refreshToken = refreshTokenService.createRefreshToken(loginUserDto.getEmail());
String jwtToken = jwtService.generateToken(authenticatedUser);
UserResponse loginResponse = new UserResponse();
loginResponse.setAccessToken(jwtToken);
loginResponse.setToken(refreshToken.getToken());
loginResponse.setExpiresIn(refreshToken.getExpiryDate());
return ResponseEntity.ok(loginResponse);
}
@PostMapping(value = "refreshToken")
public UserResponse refreshToken(@RequestBody RefreshTokenRequestDTO refreshTokenRequestDTO){
UserResponse userResponse = new UserResponse();
return refreshTokenService.findByToken(refreshTokenRequestDTO.getToken())
.map(refreshTokenService::verifyExpiration)
.map(RefreshToken::getUserInfo)
.map(userInfo -> {
String accessToken = jwtService.GenerateToken(userInfo);
userResponse.setAccessToken(accessToken);
userResponse.setToken(refreshTokenRequestDTO.getToken());
return userResponse;
// return UserResponse.builder()
// .accessToken(accessToken)
// .token(refreshTokenRequestDTO.getToken()).build();
}).orElseThrow(() ->new RuntimeException("Refresh Token is not in DB..!!"));
}
}
Step 7: Create Authentication Service
This service handles signup, user login and refresh token requests.
- Create
AuthenticationService.java
package com.example.demo.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.example.demo.Dao.UserEntity;
import com.example.demo.Dto.UserDto;
import com.example.demo.repository.UserRepository;
@Service
public class AuthenticationService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
public UserEntity signup(UserDto input) {
UserEntity user = new UserEntity();
user.setFullName(input.getFullName());
user.setEmail(input.getEmail());
user.setPassword(passwordEncoder.encode(input.getPassword()));
return userRepository.save(user);
}
public UserEntity authenticate(UserDto input) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
input.getEmail(),
input.getPassword()
)
);
return userRepository.findByEmail(input.getEmail());
}
}
Step 8: Create User Entity
We need to store user login details(like email, password) in the database.
package com.example.demo.Dao;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Table(name = "users")
@Entity
@Setter
@Getter
public class UserEntity implements UserDetails{
/**
*
*/
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(nullable = false)
private Integer id;
@Column(nullable = false)
private String fullName;
@Column(unique = true, length = 100, nullable = false)
private String email;
@Column(nullable = false)
private String password;
@CreationTimestamp
@Column(updatable = false, name = "created_at")
private Date createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private Date updatedAt;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of();
}
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Step 7: Testing the API
Login Request
curl --location 'http://localhost:8080/api/v1/login' \
--header 'Content-Type: application/json' \
--data-raw '{
"email":"abc@gmail.com",
"password":"12346"
}'
Response:
{
"token": "feac45b0-c421-42fa-aed1-ab623b86f33b",
"expiresIn": "2025-02-10T16:57:01.998264700Z",
"accessToken": "XXXXXXXXX"
}
Refreshing the Token:
curl --location 'http://localhost:8080/api/v1/refreshToken' \
--header 'Content-Type: application/json' \
--data '{
"token":"feac45b0-c421-42fa-aed1-ab623b86f33b"
}'
Response:
{
"token": "0681fdcb-4bec-49fb-b161-6a47cda79239",
"accessToken": "XXXXXXXXXXXXXXXXXXXxxxx"
}
Download complete code here