I'm trying to write my custom OAuth2 Token Revocation Endpoint as explained here but I'm facing an issue.
Inside my custom .errorResponseHandler(errorResponseHandler)
I want to catch and handle the OAuth2AuthenticationException that the authenticationProvider throw but this does not happen.
I've also change approach and I've created a custom CustomAuthenticationEntryPoint implements AuthenticationEntryPoint
and insert it inside the filterChain but nothing.
These are my classes:
filterChain
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@Profile("dev")
public class AuthServerConfigDev extends AuthServerConfigAbstract{
private static final Logger logger = LoggerFactory.getLogger(AuthServerConfigDev.class);
private final OAuth2AuthorizationService authorizationService;
private final TokenRevocationRepository tokenRevocationRepository;
public AuthServerConfigDev(@Lazy OAuth2AuthorizationService authorizationService, TokenRevocationRepository tokenRevocationRepository) {
this.authorizationService = authorizationService;
this.tokenRevocationRepository = tokenRevocationRepository;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authzServerConfigurer = new OAuth2AuthorizationServerConfigurer();
authzServerConfigurer
.oidc(withDefaults());
authzServerConfigurer
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint
.errorResponseHandler(new ClientAuthenticationFailureHandler()));
authzServerConfigurer
.tokenEndpoint(tokenEndpoint -> tokenEndpoint
.errorResponseHandler(new ClientAuthenticationFailureHandler()));
RequestMatcher endpointsMatcher = authzServerConfigurer.getEndpointsMatcher();
http.addFilterBefore(new GrantFlowFilter(new ClientAuthenticationFailureHandler()), UsernamePasswordAuthenticationFilter.class);
http
.securityMatchers(matchers -> matchers
.requestMatchers(
antMatcher(OPENAPI_JSON_URL),
antMatcher(SWAGGER_UI_URL),
antMatcher(REST_USER_PATH + "/**"),
antMatcher(REVOKE_ENDPOINT),
endpointsMatcher))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(HttpMethod.GET, OPENAPI_JSON_URL).hasAuthority("SCOPE.user")
.requestMatchers(HttpMethod.GET, SWAGGER_UI_URL).hasAuthority("SCOPE.user")
.requestMatchers(FULL_LOGIN_URL).permitAll()
.requestMatchers(REVOKE_ENDPOINT).authenticated()
.anyRequest().authenticated())
.csrf(csrf -> csrf
.ignoringRequestMatchers(endpointsMatcher))
.exceptionHandling(exceptions -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint(FULL_LOGIN_URL + ParamMissingAuth),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)))
.oauth2ResourceServer(resourceServer -> resourceServer.jwt(withDefaults()))
.with(authzServerConfigurer, (authorizationServer) ->
authorizationServer
// As defined in the guide
.tokenRevocationEndpoint(tokenRevocationEndpoint ->
tokenRevocationEndpoint
.revocationRequestConverter(new CustomRevocationRequestConverter())
.authenticationProvider(new CustomRevocationAuthenticationProvider(authorizationService, tokenRevocationRepository))
.revocationResponseHandler(new CustomRevocationResponseHandler())
.errorResponseHandler(new CustomRevocationErrorResponseHandler())));
return http.build();
}
}
CustomRevocationRequestConverter
public class CustomRevocationRequestConverter implements AuthenticationConverter {
@Override
public Authentication convert(HttpServletRequest request) {
// Extract the token and token_type_hint from the request parameters
String tokenValue = request.getParameter("token");
String tokenTypeHint = request.getParameter("token_type_hint");
if (tokenValue == null || tokenValue.isEmpty()) {
return null; // Return null if the token is not present
}
// Retrieve the client authentication from the SecurityContext
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
// Create and return an OAuth2TokenRevocationAuthenticationToken
if(tokenTypeHint != null && !tokenTypeHint.isEmpty())
return new OAuth2TokenRevocationAuthenticationToken(tokenValue, clientPrincipal, tokenTypeHint);
else {
return new OAuth2TokenRevocationAuthenticationToken(tokenValue, clientPrincipal, null);
}
}
}
CustomRevocationAuthenticationProvider
public class CustomRevocationAuthenticationProvider implements AuthenticationProvider {
private static final Logger logger = LoggerFactory.getLogger(CustomRevocationAuthenticationProvider.class);
private final OAuth2AuthorizationService authorizationService;
private final TokenRevocationRepository tokenRevocationRepository;
public CustomRevocationAuthenticationProvider(OAuth2AuthorizationService authorizationService, TokenRevocationRepository tokenRevocationRepository) {
this.authorizationService = authorizationService;
this.tokenRevocationRepository = tokenRevocationRepository;
}
@Transactional
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// Check if the authentication object is an instance of OAuth2TokenRevocationAuthenticationToken
if (authentication instanceof OAuth2TokenRevocationAuthenticationToken revocationToken) {
// Extract the token and client authentication details
String tokenValue = revocationToken.getToken(); // JWT
Authentication clientPrincipal = (Authentication) revocationToken.getPrincipal(); // Cast the principal to Authentication
String tokenTypeHint = revocationToken.getTokenTypeHint(); // Token type [access_token, refresh_token]
String principalName = clientPrincipal.getName(); // Extract the principal name
try {
if (validateAndRevokeToken(tokenValue, tokenTypeHint, principalName)) {
return new OAuth2TokenRevocationAuthenticationToken(revocationToken.getToken(), clientPrincipal, "ROLE_REVOKER");
}
} catch (OAuth2AuthenticationException e) {
// Handle authentication exceptions gracefully
logger.error("{} - {}", "CustomRevocationAuthenticationProvider.authenticate", e.getError());
throw e;
// throw new AuthenticationServiceException(e.getError().getDescription(), e);
} catch (Exception e) {
// Catch all other exceptions including SQL exceptions
logger.error("{} - {}", "CustomRevocationAuthenticationProvider.authenticate", e);
throw new OAuth2AuthenticationException(new OAuth2Error(SERVER_ERROR, "An error occurred while processing the token revocation request", null), e);
}
}
// If the token is invalid or the authentication type is not supported, return null or throw an exception
throw new AuthenticationServiceException("Unsupported authentication token");
}
// Custom validation logic for the token
private boolean validateAndRevokeToken(String tokenValue, String tokenType, String principalName) {
try {
OAuth2Authorization authorization = authorizationService.findByToken(tokenValue, OAuth2TokenType.ACCESS_TOKEN);
if (authorization == null) {
return false; // Token does not exist
}
int deletedRows;
if(tokenType != null && tokenType.equalsIgnoreCase(OAuth2TokenType.ACCESS_TOKEN.getValue())) {
Optional<Oauth2ClientIdDTO> oauth2ClientIdDTO = tokenRevocationRepository.findClientIdByAccessTokenValue(tokenValue);
validateClientIdDto(oauth2ClientIdDTO, principalName, authorization); // If a check fail it throws OAuth2AuthenticationException
deletedRows = tokenRevocationRepository.deleteByAccessTokenValue(tokenValue);
} else if (tokenType != null && tokenType.equalsIgnoreCase(OAuth2TokenType.REFRESH_TOKEN.getValue())) {
Optional<Oauth2ClientIdDTO> oauth2ClientIdDTO = tokenRevocationRepository.findClientIdByRefreshTokenValue(tokenValue);
validateClientIdDto(oauth2ClientIdDTO, principalName, authorization); // If a check fail it throws OAuth2AuthenticationException
deletedRows = tokenRevocationRepository.deleteByRefreshTokenValue(tokenValue);
} else {
Optional<Oauth2ClientIdDTO> oauth2ClientIdDTO = tokenRevocationRepository.findClientIdByAccessTokenValueOrRefreshToken(tokenValue, tokenValue);
validateClientIdDto(oauth2ClientIdDTO, principalName, authorization); // If a check fail it throws OAuth2AuthenticationException
deletedRows = tokenRevocationRepository.deleteByAccessTokenValueOrRefreshTokenValue(tokenValue, tokenValue);
}
if(deletedRows > 0)
logger.info("Token revoked.");
return true;
} catch (OAuth2AuthenticationException e) {
// Handle authentication exceptions gracefully
logger.error("{} - {}", "CustomRevocationAuthenticationProvider.validateAndRevokeToken", e.getError());
throw e;
} catch (Exception e) {
logger.error("{} - {}", "CustomRevocationAuthenticationProvider.validateAndRevokeToken", e);
throw new OAuth2AuthenticationException(new OAuth2Error(SERVER_ERROR, "An error occurred while validating the token", null), e);
}
}
private void validateClientIdDto(Optional<Oauth2ClientIdDTO> oauth2ClientIdDTO, String principalName, OAuth2Authorization authorization) {
logger.debug("Validating client id of the token to revoke.");
oauth2ClientIdDTO.ifPresentOrElse(dto -> {
if (!dto.getClient_id().equalsIgnoreCase(principalName) || !dto.getId().equalsIgnoreCase(authorization.getRegisteredClientId())) {
throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_CLIENT, "Client ID validation failed", null));
}}, () -> { // empty optional
throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_CLIENT, "Client ID not found", null));
});
}
@Override
public boolean supports(Class<?> authentication) {
return OAuth2TokenRevocationAuthenticationToken.class.isAssignableFrom(authentication);
}
}
CustomRevocationResponseHandler
public class CustomRevocationResponseHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
// Handle the success response for token revocation
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json");
response.getWriter().write("{\"message\":\"Token revoked successfully\"}");
}
}
CustomRevocationErrorResponseHandler
public class CustomRevocationErrorResponseHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
if (exception instanceof OAuth2AuthenticationException) {
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
String errorCode = error.getErrorCode();
String errorDescription = error.getDescription();
// Set the appropriate HTTP status code and error response body based on RFC 6749 Section 5.2
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType("application/json");
String errorResponse = String.format("{\"error\":\"%s\",\"error_description\":\"%s\"}", errorCode, errorDescription);
response.getWriter().write(errorResponse);
} else {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"invalid_request\",\"error_description\":\"Invalid request\"}");
}
}
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns=".0.0" xmlns:xsi=";
xsi:schemaLocation=".0.0 .0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<properties>
<java.version>17</java.version>
<mavenpiler.target>17</mavenpiler.target>
<mavenpiler.source>17</mavenpiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF8</project.reporting.outputEncoding>
<springboot.version>3.4.2</springboot.version>
<springsecurity.version>6.4.2</springsecurity.version>
<auth.server.version>1.4.1</auth.server.version>
<spring.oauth2.client.version>6.4.2</spring.oauth2.client.version>
<spring.boot.starter.log4j2.version>3.4.2</spring.boot.starter.log4j2.version>
<sql.server.jdbc.version>12.8.1.jre8</sql.server.jdbc.version>
<open.api.version>2.8.3</open.api.version>
<apachemon.lang.version>3.17.0</apachemon.lang.version>
<spring.session.jdbc.version>3.4.1</spring.session.jdbc.version>
<byte.buddy.version>1.17.0</byte.buddy.version>
<jakarta.mail.version>2.0.1</jakarta.mail.version>
</properties>
<dependencies>
<dependency>
<groupId>.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${springsecurity.version}</version>
</dependency>
<dependency>
<groupId>.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>${auth.server.version}</version>
</dependency>
<dependency>
<groupId>.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
<version>${spring.oauth2.client.version}</version>
</dependency>
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${springboot.version}</version>
<exclusions>
<exclusion>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<scope>runtime</scope>
<version>${sql.server.jdbc.version}</version>
</dependency>
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
<version>${spring.boot.starter.log4j2.version}</version>
</dependency>
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>.apachemons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${apachemon.lang.version}</version>
</dependency>
<dependency>
<groupId>.springframework.session</groupId>
<artifactId>spring-session-jdbc</artifactId>
<version>${spring.session.jdbc.version}</version>
</dependency>
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${open.api.version}</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>${byte.buddy.version}</version>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>jakarta.mail</artifactId>
<version>${jakarta.mail.version}</version>
</dependency>
<!-- TEST -->
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${springboot.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>${springsecurity.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${springboot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
<execution>
<id>build-info</id>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
I'm following the guide but I cannot find out why the OAuth2AuthenticationException if throw in my CustomRevocationAuthenticationProvider is not catch by CustomRevocationErrorResponseHandler and handle to return an error to the user. Online and asking to AI there is nothing that helps me.
If the exception happen inside CustomRevocationRequestConverter everything is ok.
Can someone tell me if I'm doing something wrong?
Thanks.