
When implementing JWT authentication, using refresh tokens provides several benefits:
First, create a model to store refresh tokens:
@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String token;
@Column(nullable = false)
private Instant expiryDate;
@OneToOne
@JoinColumn(name = "user_id", referencedColumnName = "user_id")
private Users user;
}
Create a repository to handle refresh token operations:
@Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
Optional<RefreshToken> findByToken(String token);
@Modifying
int deleteByUser(Users user);
}
Implement the refresh token service:
@Service
@RequiredArgsConstructor
public class RefreshTokenService {
@Value("${jwt.refresh.expiration}")
private Long refreshTokenDurationMs;
private final RefreshTokenRepository refreshTokenRepository;
private final UsersRepository userRepository;
public Optional<RefreshToken> findByToken(String token) {
return refreshTokenRepository.findByToken(token);
}
public RefreshToken createRefreshToken(Integer userId) {
RefreshToken refreshToken = RefreshToken.builder()
.user(userRepository.findById(userId).get())
.token(UUID.randomUUID().toString())
.expiryDate(Instant.now().plusMillis(refreshTokenDurationMs))
.build();
return refreshTokenRepository.save(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(Integer userId) {
return refreshTokenRepository.deleteByUser(userRepository.findById(userId).get());
}
}
Create DTOs for token-related operations:
public class TokenDTOs {
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class TokenRefreshRequest {
private String refreshToken;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class TokenRefreshResponse {
private String accessToken;
private String refreshToken;
private String tokenType = "Bearer";
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class AuthResponse {
private String accessToken;
private String refreshToken;
private String tokenType = "Bearer";
}
}
Enhance the AuthController to handle refresh tokens:
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtTokenUtil jwtTokenUtil;
private final RefreshTokenService refreshTokenService;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(),
request.getPassword())
);
Users user = usersRepository.findByUsername(request.getUsername())
.orElseThrow(() -> new RuntimeException("User not found"));
String accessToken = jwtTokenUtil.generateToken(user.getUsername());
RefreshToken refreshToken = refreshTokenService.createRefreshToken(user.getId());
return ResponseEntity.ok(AuthResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken.getToken())
.tokenType("Bearer")
.build());
}
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestBody TokenRefreshRequest request) {
String requestRefreshToken = request.getRefreshToken();
return refreshTokenService.findByToken(requestRefreshToken)
.map(refreshTokenService::verifyExpiration)
.map(RefreshToken::getUser)
.map(user -> {
String token = jwtTokenUtil.generateToken(user.getUsername());
return ResponseEntity.ok(TokenRefreshResponse.builder()
.accessToken(token)
.refreshToken(requestRefreshToken)
.tokenType("Bearer")
.build());
})
.orElseThrow(() -> new TokenRefreshException(requestRefreshToken,
"Refresh token is not in database!"));
}
@PostMapping("/logout")
public ResponseEntity<?> logout(@RequestBody TokenRefreshRequest request) {
refreshTokenService.findByToken(request.getRefreshToken())
.ifPresent(token ->
refreshTokenService.deleteByUserId(token.getUser().getId()));
return ResponseEntity.ok("Logged out successfully");
}
}
Create a robust API service with refresh token handling:
import axios from 'axios';
const BASE_URL = 'http://localhost:8080/api';
const api = axios.create({
baseURL: BASE_URL,
headers: {
'Content-Type': 'application/json'
}
});
// Function to refresh token
const refreshToken = async () => {
try {
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
throw new Error('No refresh token available');
}
const response = await axios.post(`${BASE_URL}/auth/refresh`, {
refreshToken: refreshToken
});
const { accessToken, refreshToken: newRefreshToken } = response.data;
localStorage.setItem('token', accessToken);
localStorage.setItem('refreshToken', newRefreshToken);
return accessToken;
} catch (error) {
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
window.location.href = '/';
throw error;
}
};
// Request interceptor
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor for automatic token refresh
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const newAccessToken = await refreshToken();
originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
return api(originalRequest);
} catch (refreshError) {
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export default api;
Update the login component to handle both tokens:
function Login() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const navigate = useNavigate();
const handleLogin = async (e) => {
e.preventDefault();
try {
const response = await api.post('/auth/login', {
username,
password
});
const { accessToken, refreshToken } = response.data;
localStorage.setItem('token', accessToken);
localStorage.setItem('refreshToken', refreshToken);
navigate('/dashboard');
} catch (error) {
console.error('Login error:', error);
}
};
// Component JSX
}
Properly handle logout with refresh tokens:
const handleLogout = async () => {
try {
const refreshToken = localStorage.getItem('refreshToken');
if (refreshToken) {
await api.post('/auth/logout', { refreshToken });
}
} catch (error) {
console.error('Logout error:', error);
} finally {
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
navigate('/login');
}
};
Add necessary properties for token configuration:
# Access Token properties
jwt.secret=aaaaaaaaaaa
jwt.expiration=3600 # 1 hour
# Refresh Token properties
jwt.refresh.expiration=604800000 # 7 days
Update security configuration to handle refresh token endpoints:
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
}
Security Considerations:
Implementation Tips:
Token Lifecycle:
Error Handling: