From a846e936e9a5b72567058b5b96f69aa383bc2a4a Mon Sep 17 00:00:00 2001 From: arfatbk Date: Thu, 11 Nov 2021 15:25:37 +0530 Subject: [PATCH] Allow Token Revocation to be customized Signed-off-by: arfatbk --- .../OAuth2AuthorizationServerConfigurer.java | 39 +++-- ...uth2TokenRevocationEndpointConfigurer.java | 160 ++++++++++++++++++ .../OAuth2TokenRevocationEndpointFilter.java | 67 +++++++- .../OAuth2TokenRevocationTests.java | 82 ++++++++- ...th2TokenRevocationEndpointFilterTests.java | 112 ++++++++++++ 5 files changed, 428 insertions(+), 32 deletions(-) create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenRevocationEndpointConfigurer.java diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java index 201387e3..493b8f13 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java @@ -40,7 +40,6 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider; -import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter; @@ -71,6 +70,7 @@ import org.springframework.util.Assert; * @see OAuth2ClientAuthenticationConfigurer * @see OAuth2AuthorizationEndpointConfigurer * @see OAuth2TokenEndpointConfigurer + * @see OAuth2TokenRevocationEndpointConfigurer * @see OidcConfigurer * @see RegisteredClientRepository * @see OAuth2AuthorizationService @@ -85,15 +85,14 @@ public final class OAuth2AuthorizationServerConfigurer, AbstractOAuth2Configurer> configurers = createConfigurers(); private RequestMatcher tokenIntrospectionEndpointMatcher; - private RequestMatcher tokenRevocationEndpointMatcher; private RequestMatcher jwkSetEndpointMatcher; private RequestMatcher authorizationServerMetadataEndpointMatcher; private final RequestMatcher endpointsMatcher = (request) -> getRequestMatcher(OAuth2AuthorizationEndpointConfigurer.class).matches(request) || getRequestMatcher(OAuth2TokenEndpointConfigurer.class).matches(request) || + getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class).matches(request) || getRequestMatcher(OidcConfigurer.class).matches(request) || this.tokenIntrospectionEndpointMatcher.matches(request) || - this.tokenRevocationEndpointMatcher.matches(request) || this.jwkSetEndpointMatcher.matches(request) || this.authorizationServerMetadataEndpointMatcher.matches(request); @@ -178,6 +177,18 @@ public final class OAuth2AuthorizationServerConfigurer tokenRevocationEndpoint(Customizer tokenRevocationEndpointCustomizer) { + tokenRevocationEndpointCustomizer.customize(getConfigurer(OAuth2TokenRevocationEndpointConfigurer.class)); + return this; + } + /** * Configures OpenID Connect 1.0 support. * @@ -212,19 +223,14 @@ public final class OAuth2AuthorizationServerConfigurer exceptionHandling = builder.getConfigurer(ExceptionHandlingConfigurer.class); if (exceptionHandling != null) { exceptionHandling.defaultAuthenticationEntryPointFor( new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), new OrRequestMatcher( getRequestMatcher(OAuth2TokenEndpointConfigurer.class), - this.tokenIntrospectionEndpointMatcher, - this.tokenRevocationEndpointMatcher) + getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class), + this.tokenIntrospectionEndpointMatcher) ); } @@ -249,8 +255,8 @@ public final class OAuth2AuthorizationServerConfigurer authenticationProviders = new LinkedList<>(); + private AuthenticationConverter revocationRequestConverter; + private AuthenticationSuccessHandler revocationResponseHandler; + private AuthenticationFailureHandler errorResponseHandler; + private RequestMatcher requestMatcher; + + /** + * Restrict for internal use only. + */ + OAuth2TokenRevocationEndpointConfigurer(ObjectPostProcessor objectPostProcessor) { + super(objectPostProcessor); + } + + /** + * Sets the {@link AuthenticationConverter} used when attempting to extract a Revoke Token Request from {@link HttpServletRequest} + * to an instance of {@link OAuth2TokenRevocationAuthenticationToken} used for authenticating the client. + * + * @param revocationRequestConverter the {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest} + * @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further configuration + */ + public OAuth2TokenRevocationEndpointConfigurer revocationRequestConverter(AuthenticationConverter revocationRequestConverter) { + this.revocationRequestConverter = revocationRequestConverter; + return this; + } + + /** + * Adds an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2TokenRevocationAuthenticationToken}. + * + * @param authenticationProvider an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2TokenRevocationAuthenticationToken} + * @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further configuration + */ + public OAuth2TokenRevocationEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) { + this.authenticationProviders.add(authenticationProvider); + return this; + } + + /** + * Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenRevocationAuthenticationToken}. + * + * @param revocationResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenRevocationAuthenticationToken} + * @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further configuration + */ + public OAuth2TokenRevocationEndpointConfigurer revocationResponseHandler(AuthenticationSuccessHandler revocationResponseHandler) { + this.revocationResponseHandler = revocationResponseHandler; + return this; + } + + /** + * Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException} + * and returning the {@link OAuth2Error Error Response}. + * + * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException} + * @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further configuration + */ + public OAuth2TokenRevocationEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) { + this.errorResponseHandler = errorResponseHandler; + return this; + } + + @Override + > void init(B builder) { + ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder); + this.requestMatcher = new AntPathRequestMatcher( + providerSettings.getTokenRevocationEndpoint(), HttpMethod.POST.name()); + + List authenticationProviders = + !this.authenticationProviders.isEmpty() ? + this.authenticationProviders : + createDefaultAuthenticationProviders(builder); + authenticationProviders.forEach(authenticationProvider -> + builder.authenticationProvider(postProcess(authenticationProvider))); + } + + @Override + > void configure(B builder) { + AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); + ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder); + + OAuth2TokenRevocationEndpointFilter revocationEndpointFilter = + new OAuth2TokenRevocationEndpointFilter( + authenticationManager, + providerSettings.getTokenRevocationEndpoint() + ); + if (this.revocationRequestConverter != null) { + revocationEndpointFilter.setRevocationRequestConverter(this.revocationRequestConverter); + } + if (this.revocationResponseHandler != null) { + revocationEndpointFilter.setRevocationResponseHandler(this.revocationResponseHandler); + } + if (this.errorResponseHandler != null) { + revocationEndpointFilter.setErrorResponseHandler(this.errorResponseHandler); + } + builder.addFilterAfter(postProcess(revocationEndpointFilter), FilterSecurityInterceptor.class); + } + + @Override + RequestMatcher getRequestMatcher() { + return this.requestMatcher; + } + + private > List createDefaultAuthenticationProviders(B builder) { + List authenticationProviders = new ArrayList<>(); + + OAuth2TokenRevocationAuthenticationProvider tokenRevocationAuthenticationProvider = + new OAuth2TokenRevocationAuthenticationProvider(OAuth2ConfigurerUtils.getAuthorizationService(builder)); + authenticationProviders.add(tokenRevocationAuthenticationProvider); + + return authenticationProviders; + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenRevocationEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenRevocationEndpointFilter.java index cbf221a4..a45e1b87 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenRevocationEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenRevocationEndpointFilter.java @@ -21,14 +21,13 @@ import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; - -import org.springframework.core.convert.converter.Converter; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; @@ -37,6 +36,9 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationToken; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; @@ -49,6 +51,7 @@ import org.springframework.web.filter.OncePerRequestFilter; * * @author Vivek Babu * @author Joe Grandja + * @author Arfat Chaus * @see OAuth2TokenRevocationAuthenticationProvider * @see Section 2 Token Revocation * @see Section 2.1 Revocation Request @@ -62,11 +65,14 @@ public final class OAuth2TokenRevocationEndpointFilter extends OncePerRequestFil private final AuthenticationManager authenticationManager; private final RequestMatcher tokenRevocationEndpointMatcher; - private final Converter tokenRevocationAuthenticationConverter = + private AuthenticationConverter revocationRequestConverter = new DefaultTokenRevocationAuthenticationConverter(); private final HttpMessageConverter errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter(); + private AuthenticationSuccessHandler revocationResponseHandler = this::sendRevocationSuccessResponse; + private AuthenticationFailureHandler errorResponseHandler = this::sendErrorResponse; + /** * Constructs an {@code OAuth2TokenRevocationEndpointFilter} using the provided parameters. * @@ -101,16 +107,59 @@ public final class OAuth2TokenRevocationEndpointFilter extends OncePerRequestFil } try { - this.authenticationManager.authenticate( - this.tokenRevocationAuthenticationConverter.convert(request)); - response.setStatus(HttpStatus.OK.value()); + Authentication tokenRevocationAuthentication = this.revocationRequestConverter.convert(request); + + Authentication authentication = this.authenticationManager.authenticate(tokenRevocationAuthentication); + this.revocationResponseHandler.onAuthenticationSuccess(request, response, authentication); } catch (OAuth2AuthenticationException ex) { SecurityContextHolder.clearContext(); - sendErrorResponse(response, ex.getError()); + this.errorResponseHandler.onAuthenticationFailure(request, response, ex); } } - private void sendErrorResponse(HttpServletResponse response, OAuth2Error error) throws IOException { + + /** + * Sets the {@link AuthenticationConverter} used when attempting to extract a Revoke Token Request from {@link HttpServletRequest} + * to an instance of {@link OAuth2TokenRevocationAuthenticationToken} used for authenticating the client. + * + * @param revocationRequestConverter the {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest} + * @since 0.2.2 + */ + public void setRevocationRequestConverter(AuthenticationConverter revocationRequestConverter) { + Assert.notNull(revocationRequestConverter, "revocationRequestConverter cannot be null"); + this.revocationRequestConverter = revocationRequestConverter; + } + + /** + * Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenRevocationAuthenticationToken}. + * + * @param revocationResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenRevocationAuthenticationToken} + * @since 0.2.2 + */ + public void setRevocationResponseHandler(AuthenticationSuccessHandler revocationResponseHandler) { + Assert.notNull(revocationResponseHandler, "revocationResponseHandler cannot be null"); + this.revocationResponseHandler = revocationResponseHandler; + } + + /** + * Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException} + * and returning the {@link OAuth2Error Error Response}. + * + * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException} + * @since 0.2.2 + */ + public void setErrorResponseHandler(AuthenticationFailureHandler errorResponseHandler) { + Assert.notNull(errorResponseHandler, "errorResponseHandler cannot be null"); + this.errorResponseHandler = errorResponseHandler; + } + + private void sendRevocationSuccessResponse(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + response.setStatus(HttpStatus.OK.value()); + } + + private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) throws IOException { + OAuth2Error error = ((OAuth2AuthenticationException) exception).getError(); ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); httpResponse.setStatusCode(HttpStatus.BAD_REQUEST); this.errorHttpResponseConverter.write(error, null, httpResponse); @@ -123,7 +172,7 @@ public final class OAuth2TokenRevocationEndpointFilter extends OncePerRequestFil } private static class DefaultTokenRevocationAuthenticationConverter - implements Converter { + implements AuthenticationConverter { @Override public Authentication convert(HttpServletRequest request) { diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenRevocationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenRevocationTests.java index 640aa4e3..89ff1d3c 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenRevocationTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenRevocationTests.java @@ -31,19 +31,25 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpHeaders; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.Authentication; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.core.AbstractOAuth2Token; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.OAuth2TokenType; @@ -53,6 +59,8 @@ import org.springframework.security.oauth2.server.authorization.JdbcOAuth2Author import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationToken; import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; @@ -60,11 +68,21 @@ import org.springframework.security.oauth2.server.authorization.client.Registere import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; import org.springframework.security.oauth2.server.authorization.jackson2.TestingAuthenticationTokenMixin; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.test.web.servlet.MockMvc; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -78,7 +96,10 @@ public class OAuth2TokenRevocationTests { private static EmbeddedDatabase db; private static JWKSource jwkSource; private static ProviderSettings providerSettings; - + private static AuthenticationConverter revocationRequestConverter; + private static AuthenticationProvider authenticationProvider; + private static AuthenticationSuccessHandler revocationResponseHandler; + private static AuthenticationFailureHandler errorResponseHandler; @Rule public final SpringTestRule spring = new SpringTestRule(); @@ -99,6 +120,10 @@ public class OAuth2TokenRevocationTests { JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK); jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); providerSettings = ProviderSettings.builder().tokenRevocationEndpoint("/test/revoke").build(); + revocationRequestConverter = mock(AuthenticationConverter.class); + authenticationProvider = mock(AuthenticationProvider.class); + revocationResponseHandler = mock(AuthenticationSuccessHandler.class); + errorResponseHandler = mock(AuthenticationFailureHandler.class); db = new EmbeddedDatabaseBuilder() .generateUniqueName(true) .setType(EmbeddedDatabaseType.HSQL) @@ -152,10 +177,35 @@ public class OAuth2TokenRevocationTests { } @Test - public void requestWhenRevokeAccessTokenCustomEndpointThenRevoked() throws Exception { + public void requestWhenRevokeAccessTokenEndpointCustomizedThenUsed() throws Exception { this.spring.register(AuthorizationServerConfigurationCustomEndpoints.class).autowire(); - assertRevokeAccessTokenThenRevoked(providerSettings.getTokenRevocationEndpoint()); + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + this.registeredClientRepository.save(registeredClient); + Authentication clientPrincipal = new OAuth2ClientAuthenticationToken( + registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); + + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build(); + OAuth2AccessToken token = authorization.getAccessToken().getToken(); + OAuth2TokenType tokenType = OAuth2TokenType.ACCESS_TOKEN; + this.authorizationService.save(authorization); + + OAuth2TokenRevocationAuthenticationToken tokenRevocationAuthenticationResult = + new OAuth2TokenRevocationAuthenticationToken(token, clientPrincipal); + + when(revocationRequestConverter.convert(any())).thenReturn(tokenRevocationAuthenticationResult); + when(authenticationProvider.supports(eq(OAuth2TokenRevocationAuthenticationToken.class))).thenReturn(true); + when(authenticationProvider.authenticate(any())).thenReturn(tokenRevocationAuthenticationResult); + + this.mvc.perform(post(providerSettings.getTokenRevocationEndpoint()) + .params(getTokenRevocationRequestParameters(token, tokenType)) + .header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth( + registeredClient.getClientId(), registeredClient.getClientSecret()))) + .andExpect(status().isOk()); + + verify(revocationRequestConverter).convert(any()); + verify(authenticationProvider).authenticate(eq(tokenRevocationAuthenticationResult)); + verify(revocationResponseHandler).onAuthenticationSuccess(any(), any(), eq(tokenRevocationAuthenticationResult)); } private void assertRevokeAccessTokenThenRevoked(String tokenRevocationEndpointUri) throws Exception { @@ -255,6 +305,32 @@ public class OAuth2TokenRevocationTests { @Import(OAuth2AuthorizationServerConfiguration.class) static class AuthorizationServerConfigurationCustomEndpoints extends AuthorizationServerConfiguration { + // @formatter:off + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = + new OAuth2AuthorizationServerConfigurer<>(); + authorizationServerConfigurer + .tokenRevocationEndpoint(tokenRevocationEndpoint -> + tokenRevocationEndpoint + .revocationRequestConverter(revocationRequestConverter) + .authenticationProvider(authenticationProvider) + .revocationResponseHandler(revocationResponseHandler) + .errorResponseHandler(errorResponseHandler)); + RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); + + http + .requestMatcher(endpointsMatcher) + .authorizeRequests(authorizeRequests -> + authorizeRequests.anyRequest().authenticated() + ) + .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) + .apply(authorizationServerConfigurer); + return http.build(); + } + // @formatter:on + @Bean ProviderSettings providerSettings() { return providerSettings; diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenRevocationEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenRevocationEndpointFilterTests.java index ac2bf732..c5f71ee0 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenRevocationEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenRevocationEndpointFilterTests.java @@ -40,6 +40,7 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2TokenType; @@ -49,6 +50,9 @@ import org.springframework.security.oauth2.server.authorization.authentication.O import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationToken; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -96,6 +100,27 @@ public class OAuth2TokenRevocationEndpointFilterTests { .hasMessage("tokenRevocationEndpointUri cannot be empty"); } + @Test + public void setRevocationRequestConverterWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.filter.setRevocationRequestConverter(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("revocationRequestConverter cannot be null"); + } + + @Test + public void setRevocationResponseHandlerWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.filter.setRevocationResponseHandler(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("revocationResponseHandler cannot be null"); + } + + @Test + public void setErrorResponseHandlerWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.filter.setErrorResponseHandler(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("errorResponseHandler cannot be null"); + } + @Test public void doFilterWhenNotTokenRevocationRequestThenNotProcessed() throws Exception { String requestUri = "/path"; @@ -177,6 +202,93 @@ public class OAuth2TokenRevocationEndpointFilterTests { assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); } + @Test + public void doFilterWhenCustomAuthenticationConverterThenUsed() throws Exception { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + Authentication clientPrincipal = new OAuth2ClientAuthenticationToken( + registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); + OAuth2AccessToken accessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, "token", + Instant.now(), Instant.now().plus(Duration.ofHours(1)), + new HashSet<>(Arrays.asList("scope1", "scope2"))); + OAuth2TokenRevocationAuthenticationToken tokenRevocationAuthentication = + new OAuth2TokenRevocationAuthenticationToken( + accessToken, clientPrincipal); + + AuthenticationConverter authenticationConverter = mock(AuthenticationConverter.class); + when(authenticationConverter.convert(any())).thenReturn(tokenRevocationAuthentication); + this.filter.setRevocationRequestConverter(authenticationConverter); + + when(this.authenticationManager.authenticate(any())).thenReturn(tokenRevocationAuthentication); + + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(clientPrincipal); + SecurityContextHolder.setContext(securityContext); + + MockHttpServletRequest request = createTokenRevocationRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + verify(authenticationConverter).convert(any()); + } + + @Test + public void doFilterWhenCustomAuthenticationSuccessHandlerThenUsed() throws Exception { + AuthenticationSuccessHandler authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class); + this.filter.setRevocationResponseHandler(authenticationSuccessHandler); + + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + Authentication clientPrincipal = new OAuth2ClientAuthenticationToken( + registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); + OAuth2AccessToken accessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, "token", + Instant.now(), Instant.now().plus(Duration.ofHours(1)), + new HashSet<>(Arrays.asList("scope1", "scope2"))); + OAuth2TokenRevocationAuthenticationToken tokenRevocationAuthentication = + new OAuth2TokenRevocationAuthenticationToken( + accessToken, clientPrincipal); + + when(this.authenticationManager.authenticate(any())).thenReturn(tokenRevocationAuthentication); + + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(clientPrincipal); + SecurityContextHolder.setContext(securityContext); + + MockHttpServletRequest request = createTokenRevocationRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), any()); + } + + @Test + public void doFilterWhenCustomAuthenticationFailureHandlerThenUsed() throws Exception { + AuthenticationFailureHandler authenticationFailureHandler = mock(AuthenticationFailureHandler.class); + this.filter.setErrorResponseHandler(authenticationFailureHandler); + + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + Authentication clientPrincipal = new OAuth2ClientAuthenticationToken( + registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); + + when(this.authenticationManager.authenticate(any())).thenThrow(OAuth2AuthenticationException.class); + + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(clientPrincipal); + SecurityContextHolder.setContext(securityContext); + + MockHttpServletRequest request = createTokenRevocationRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), any()); + } + private void doFilterWhenTokenRevocationRequestInvalidParameterThenError(String parameterName, String errorCode, Consumer requestConsumer) throws Exception {