最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

java - OAuth2 Token Revocation Endpoint - Stack Overflow

programmeradmin6浏览0评论

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.

发布评论

评论列表(0)

  1. 暂无评论