Description
Summary
After migration from Spring Security 4.0.X to 4.1.0.RELEASE I have a problem with deserialization of tokens already stored in the database.
UsernamePasswordAuthenticationToken has serialVersionUID set to SpringSecurityCoreVersion.SERIAL_VERSION_UID;
which after upgrade of the release version leads to problems like this one below:
java.lang.IllegalArgumentException: java.io.InvalidClassException: org.springframework.security.authentication.UsernamePasswordAuthenticationToken; local class incompatible: stream classdesc serialVersionUID = 400, local class serialVersionUID = 410;
It is a bit strange, because nothing was changed in this class but serial version was updated!
In the result of this exception which is logged and swallowed, another NullPointerException is thrown and finally http 500 response is produced. But the worst thing is that all of oauth2 clients have to refresh them refreshTokens often by logging in again.
Actual Behavior
When IllegalArgumentException occured JdbcTokenStore removes old token (valid but with different serialVersionUID) and returns null. This looks correct but returned null is not handled anywhere further in the code.
public OAuth2Authentication readAuthenticationForRefreshToken(String value) {
OAuth2Authentication authentication = null;
try {
authentication = jdbcTemplate.queryForObject(selectRefreshTokenAuthenticationSql,
new RowMapper<OAuth2Authentication>() {
public OAuth2Authentication mapRow(ResultSet rs, int rowNum) throws SQLException {
return deserializeAuthentication(rs.getBytes(2));
}
}, extractTokenKey(value));
}
catch (EmptyResultDataAccessException e) {
if (LOG.isInfoEnabled()) {
LOG.info("Failed to find access token for token " + value);
}
}
catch (IllegalArgumentException e) {
LOG.warn("Failed to deserialize access token for " + value, e);
removeRefreshToken(value);
}
return authentication;
}
For instance in DefaultTokenServices
@Transactional(noRollbackFor={InvalidTokenException.class, InvalidGrantException.class})
public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest)
throws AuthenticationException {
if (!supportRefreshToken) {
throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
}
OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(refreshTokenValue);
if (refreshToken == null) {
throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
}
///////////////////////////////////////////////////////////////////
OAuth2Authentication authentication = tokenStore.readAuthenticationForRefreshToken(refreshToken);
if (this.authenticationManager != null && !authentication.isClientOnly()) { ////// throws NullPointerException
////////////////////////////////////////////////////////
}
Finally, the request ends up with HTTP 500 and log:
org.springframework.security.oauth2.provider.endpoint.TokenEndpoint handleException: Handling error: NullPointerException, null
Expected Behavior
IMHO this situation should return the same result like when a refresh token is expired, but not http 500.
I also think UsernamePasswordAuthenticationToken should not have serialVersionUID that depends on SpringSecurityCoreVersion.
Configuration
Spring 4.2.6.RELEASE, Spring Security 4.1.0.RELEASE, Spring Security Oauth2 2.0.10.RELEASE
Version
4.1.0.RELEASE