I am using jwt to authenticate in my spring boot app. These are methods in AuthenticationService:
public AuthenticationResponse authenticate(AuthenticationRequest request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(),
request.getPassword()
)
);
User user = userRepository.findByEmail(request.getEmail()).orElseThrow(UserNotFoundException::new);
String jwtToken = jwtService.generateToken(user);
String refreshToken = jwtService.generateRefreshToken(user);
revokeAllUserTokens(user);
saveUserToken(user, jwtToken);
return AuthenticationResponse.builder()
.accessToken(jwtToken)
.refreshToken(refreshToken)
.build();
}
private void revokeAllUserTokens(User user) {
List<Token> validUserTokens = tokenRepository.findAllValidTokenByUser(user.getId());
if(validUserTokens.isEmpty())
return;
validUserTokens.forEach(token -> {
token.setExpired(true);
token.setRevoked(true);
});
tokenRepository.saveAll(validUserTokens);
}
private void saveUserToken(User user, String jwtToken) {
Token token = Token.builder()
.user(user)
.token(jwtToken)
.tokenType(TokenType.BEARER)
.expired(false)
.revoked(false)
.build();
tokenRepository.save(token);
}
and in JwtService:
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return buildToken(extraClaims, userDetails, jwtConfig.getExpiration());
}
public String generateRefreshToken(UserDetails userDetails) {
return buildToken(new HashMap<>(), userDetails, jwtConfig.getRefreshTokenExpiration());
}
and Token inside database:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "jwt_tokens")
public class Token {
@Id
@GeneratedValue
public Long id;
@Column(name = "token", unique = true)
public String token;
@Enumerated(EnumType.STRING)
public TokenType tokenType = TokenType.BEARER;
public boolean revoked;
public boolean expired;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
public User user;
}
rest of the implementation is rather self explanatory.
It works fine, but when there are multiple authentication request (when I rapidly send multiple requests), there is 500 status response, and an exception is thrown (method authenticate in AuthenticationService) is called:
org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Unique index or primary key violation: "PUBLIC.CONSTRAINT_INDEX_4 ON PUBLIC.JWT_TOKENS(TOKEN NULLS FIRST) VALUES ( /* 7 */ 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJtYWx5enNAZW1haWwuY29tIiwiaWF0IjoxNjkwMjE4NDU3LCJleHAiOjE2OTAzMDQ4NTd9.8aePz6jl3JPvsnDFVpXxtzYfsxLN0ZqIUg0FsK_XDaE' )"; SQL statement:
insert into jwt_tokens (expired,revoked,token,token_type,user_id,id) values (?,?,?,?,?,?) [23505-214]
>Solution :
JWT tokens, by design, use UNIX Epoch time in seconds for iat (issued at) time.
I reckon a good hack would be to include additional information in extraClaims when generating JWT tokens, for example the current time in miliseconds. This doesn’t change the usage of tokens as they still get hashed.
In terms of your implementation, a possible change would be:
String jwtToken = jwtService.generateToken(Map.of("milis", new Date(System.currentTimeMillis())), user);
String refreshToken = jwtService.generateRefreshToken(Map.of("milis", new Date(System.currentTimeMillis())), user);