Access Token (access token) — is a key that lets a user interact with protected resources. However, for security reasons its lifetime is limited. For example, an Access Token might be valid for only 15 minutes. After the token expires, all protected requests will start returning 401 Unauthorized errors. In that case the user is forced to re-authenticate, which will clearly annoy them (and they probably won't give your app 5 stars in the App Store).
Solution: Refresh tokens
A Refresh token is meant to refresh an expired Access Token without requiring the user to re-enter credentials. This makes the user's interaction with the app smooth and the system more convenient.
How does it work?
- On successful authentication the server issues two tokens:
Access Token— for accessing protected resources.Refresh Token— for obtaining a new Access Token when the old one expires.
- When the Access Token expires, the client (for example, your mobile or web app) sends the Refresh Token to the server.
- The server validates the Refresh Token and, if it's valid, issues a new Access Token.
- The user continues working without re-authenticating.
Configuring and using Refresh tokens in Spring
Let's build a Spring Boot app that uses Refresh tokens to renew Access Tokens. For this you need an existing app with JWT authentication. If you did the exercises from previous lectures, you already have a basic JWT setup. If not, I recommend going back and setting that up first.
1. Generating Refresh tokens
Start by changing the token issuance logic. Now our authentication endpoint will return two tokens.
Example token generation code:
@RestController
@RequestMapping("/auth")
public class AuthController {
private final JwtService jwtService; // Service for working with JWT
private final UserService userService;
@PostMapping("/login")
public ResponseEntity
login(@RequestBody LoginRequest request) {
// Check user's credentials
User user = userService.authenticate(request.getUsername(), request.getPassword());
// Generate Access and Refresh tokens
String accessToken = jwtService.generateAccessToken(user);
String refreshToken = jwtService.generateRefreshToken(user);
// Return them in the response
return ResponseEntity.ok(Map.of(
"accessToken", accessToken,
"refreshToken", refreshToken
));
}
}
Note that we now use two different methods to generate Access and Refresh tokens. Access Token usually has a short lifetime (for example, 15 minutes), whereas Refresh Token is longer-lived (for example, 7 days).
2. Adding an endpoint to refresh tokens
Now we'll add an endpoint that accepts a Refresh Token and returns a new Access Token.
Implementation example:
@RestController
@RequestMapping("/auth")
public class AuthController {
private final JwtService jwtService;
@PostMapping("/refresh")
public ResponseEntity<?> refresh(@RequestBody Map<String, String> body) {
String refreshToken = body.get("refreshToken");
// Validate the Refresh token
if (!jwtService.isValidRefreshToken(refreshToken)) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid Refresh Token");
}
// Generate a new Access Token
String newAccessToken = jwtService.generateAccessTokenFromRefreshToken(refreshToken);
return ResponseEntity.ok(Map.of(
"accessToken", newAccessToken
));
}
}
3. Validation and generation logic in JwtService
We'll add two new methods to JwtService. One for validating the Refresh token, and another for generating a new Access Token based on it.
Example:
@Service
public class JwtService {
private final String secretKey = "your-secret-key"; // Secret key for signing tokens
public boolean isValidRefreshToken(String refreshToken) {
try {
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(refreshToken)
.getBody();
// Make sure it's actually a Refresh token
String tokenType = claims.get("type", String.class);
return "refresh".equals(tokenType);
} catch (JwtException e) {
return false;
}
}
public String generateAccessTokenFromRefreshToken(String refreshToken) {
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(refreshToken)
.getBody();
String username = claims.getSubject();
// Create a new Access Token
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 15 * 60 * 1000)) // 15 minutes
.signWith(SignatureAlgorithm.HS512, secretKey)
.compact();
}
}
Here it's important to ensure the Refresh token is indeed a Refresh token (for example, by checking an extra "type" field in the payload).
4. Secure handling of Refresh tokens
Refresh tokens are more exposed because they live longer. Here are some recommendations to improve security:
- Client storage: never store Refresh tokens in
localStorage. Use HTTP-only cookies. - HTTPS protocol: always use a secure connection when transmitting tokens.
- Token rotation: after using a Refresh token to get a new Access Token, you can generate a new Refresh Token and return it to the client. This reduces the chance of compromise.
- Revoked token list: if a token leak is detected, you must be able to revoke the Refresh token. This is usually done by tracking valid tokens in a database.
5. Error handling
Common problems with Refresh tokens:
- Expired token: return
401 Unauthorizedand ask the user to re-authenticate. - Invalid token: if the token is tampered with, reject it.
- Token forgery: use digital signatures (HMAC or RSA) to verify authenticity.
Example error handling:
if (!jwtService.isValidRefreshToken(refreshToken)) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid Refresh Token");
}
Practical applications
It's hard to imagine a modern app without Refresh tokens. For example:
- In mobile apps, where it's important to keep the user logged in for long periods.
- In web apps that need to support background requests (for example, fetching a new feed).
- Employers value knowledge of Refresh tokens because it shows understanding of security principles, user experience, and token handling.
The official Spring Security documentation on working with JWT and OAuth2 is available here.
Now you're ready to use Refresh tokens in your apps and can significantly improve both user experience and security!
GO TO FULL VERSION