Allow Token Revocation to be customized
Signed-off-by: arfatbk <arfatbk@gmail.com>
This commit is contained in:
@@ -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<B extends HttpSecurityBui
|
||||
|
||||
private final Map<Class<? extends AbstractOAuth2Configurer>, 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<B extends HttpSecurityBui
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the OAuth 2.0 Token Revocation Endpoint.
|
||||
*
|
||||
* @param tokenRevocationEndpointCustomizer the {@link Customizer} providing access to the {@link OAuth2TokenRevocationEndpointConfigurer}
|
||||
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
|
||||
* @since 0.2.2
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer<B> tokenRevocationEndpoint(Customizer<OAuth2TokenRevocationEndpointConfigurer> tokenRevocationEndpointCustomizer) {
|
||||
tokenRevocationEndpointCustomizer.customize(getConfigurer(OAuth2TokenRevocationEndpointConfigurer.class));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures OpenID Connect 1.0 support.
|
||||
*
|
||||
@@ -212,19 +223,14 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
|
||||
OAuth2ConfigurerUtils.getAuthorizationService(builder));
|
||||
builder.authenticationProvider(postProcess(tokenIntrospectionAuthenticationProvider));
|
||||
|
||||
OAuth2TokenRevocationAuthenticationProvider tokenRevocationAuthenticationProvider =
|
||||
new OAuth2TokenRevocationAuthenticationProvider(
|
||||
OAuth2ConfigurerUtils.getAuthorizationService(builder));
|
||||
builder.authenticationProvider(postProcess(tokenRevocationAuthenticationProvider));
|
||||
|
||||
ExceptionHandlingConfigurer<B> 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<B extends HttpSecurityBui
|
||||
// OAuth2TokenEndpointFilter, OAuth2TokenIntrospectionEndpointFilter and OAuth2TokenRevocationEndpointFilter
|
||||
private final RequestMatcher clientAuthenticationRequestMatcher = new OrRequestMatcher(
|
||||
getRequestMatcher(OAuth2TokenEndpointConfigurer.class),
|
||||
OAuth2AuthorizationServerConfigurer.this.tokenIntrospectionEndpointMatcher,
|
||||
OAuth2AuthorizationServerConfigurer.this.tokenRevocationEndpointMatcher);
|
||||
getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class),
|
||||
OAuth2AuthorizationServerConfigurer.this.tokenIntrospectionEndpointMatcher);
|
||||
|
||||
// JwtAuthenticationToken is @Transient and is accepted by
|
||||
// OidcUserInfoEndpointFilter and OidcClientRegistrationEndpointFilter
|
||||
@@ -341,12 +347,6 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
|
||||
providerSettings.getTokenIntrospectionEndpoint());
|
||||
builder.addFilterAfter(postProcess(tokenIntrospectionEndpointFilter), FilterSecurityInterceptor.class);
|
||||
|
||||
OAuth2TokenRevocationEndpointFilter tokenRevocationEndpointFilter =
|
||||
new OAuth2TokenRevocationEndpointFilter(
|
||||
authenticationManager,
|
||||
providerSettings.getTokenRevocationEndpoint());
|
||||
builder.addFilterAfter(postProcess(tokenRevocationEndpointFilter), FilterSecurityInterceptor.class);
|
||||
|
||||
NimbusJwkSetEndpointFilter jwkSetEndpointFilter =
|
||||
new NimbusJwkSetEndpointFilter(
|
||||
OAuth2ConfigurerUtils.getJwkSource(builder),
|
||||
@@ -365,6 +365,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
|
||||
configurers.put(OAuth2ClientAuthenticationConfigurer.class, new OAuth2ClientAuthenticationConfigurer(this::postProcess));
|
||||
configurers.put(OAuth2AuthorizationEndpointConfigurer.class, new OAuth2AuthorizationEndpointConfigurer(this::postProcess));
|
||||
configurers.put(OAuth2TokenEndpointConfigurer.class, new OAuth2TokenEndpointConfigurer(this::postProcess));
|
||||
configurers.put(OAuth2TokenRevocationEndpointConfigurer.class, new OAuth2TokenRevocationEndpointConfigurer(this::postProcess));
|
||||
configurers.put(OidcConfigurer.class, new OidcConfigurer(this::postProcess));
|
||||
return configurers;
|
||||
}
|
||||
@@ -381,8 +382,6 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
|
||||
private void initEndpointMatchers(ProviderSettings providerSettings) {
|
||||
this.tokenIntrospectionEndpointMatcher = new AntPathRequestMatcher(
|
||||
providerSettings.getTokenIntrospectionEndpoint(), HttpMethod.POST.name());
|
||||
this.tokenRevocationEndpointMatcher = new AntPathRequestMatcher(
|
||||
providerSettings.getTokenRevocationEndpoint(), HttpMethod.POST.name());
|
||||
this.jwkSetEndpointMatcher = new AntPathRequestMatcher(
|
||||
providerSettings.getJwkSetEndpoint(), HttpMethod.GET.name());
|
||||
this.authorizationServerMetadataEndpointMatcher = new AntPathRequestMatcher(
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
|
||||
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
|
||||
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 javax.servlet.http.HttpServletRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Configurer for the OAuth 2.0 Token Revocation Endpoint.
|
||||
*
|
||||
* @author Arfat Chaus
|
||||
* @since 0.2.2
|
||||
* @see OAuth2AuthorizationServerConfigurer#tokenRevocationEndpoint
|
||||
* @see OAuth2TokenRevocationEndpointFilter
|
||||
*/
|
||||
public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth2Configurer {
|
||||
private final List<AuthenticationProvider> authenticationProviders = new LinkedList<>();
|
||||
private AuthenticationConverter revocationRequestConverter;
|
||||
private AuthenticationSuccessHandler revocationResponseHandler;
|
||||
private AuthenticationFailureHandler errorResponseHandler;
|
||||
private RequestMatcher requestMatcher;
|
||||
|
||||
/**
|
||||
* Restrict for internal use only.
|
||||
*/
|
||||
OAuth2TokenRevocationEndpointConfigurer(ObjectPostProcessor<Object> 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
|
||||
<B extends HttpSecurityBuilder<B>> void init(B builder) {
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
|
||||
this.requestMatcher = new AntPathRequestMatcher(
|
||||
providerSettings.getTokenRevocationEndpoint(), HttpMethod.POST.name());
|
||||
|
||||
List<AuthenticationProvider> authenticationProviders =
|
||||
!this.authenticationProviders.isEmpty() ?
|
||||
this.authenticationProviders :
|
||||
createDefaultAuthenticationProviders(builder);
|
||||
authenticationProviders.forEach(authenticationProvider ->
|
||||
builder.authenticationProvider(postProcess(authenticationProvider)));
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> 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 <B extends HttpSecurityBuilder<B>> List<AuthenticationProvider> createDefaultAuthenticationProviders(B builder) {
|
||||
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
|
||||
OAuth2TokenRevocationAuthenticationProvider tokenRevocationAuthenticationProvider =
|
||||
new OAuth2TokenRevocationAuthenticationProvider(OAuth2ConfigurerUtils.getAuthorizationService(builder));
|
||||
authenticationProviders.add(tokenRevocationAuthenticationProvider);
|
||||
|
||||
return authenticationProviders;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 <a target="_blank" href="https://tools.ietf.org/html/rfc7009#section-2">Section 2 Token Revocation</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7009#section-2.1">Section 2.1 Revocation Request</a>
|
||||
@@ -62,11 +65,14 @@ public final class OAuth2TokenRevocationEndpointFilter extends OncePerRequestFil
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final RequestMatcher tokenRevocationEndpointMatcher;
|
||||
private final Converter<HttpServletRequest, Authentication> tokenRevocationAuthenticationConverter =
|
||||
private AuthenticationConverter revocationRequestConverter =
|
||||
new DefaultTokenRevocationAuthenticationConverter();
|
||||
private final HttpMessageConverter<OAuth2Error> 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<HttpServletRequest, Authentication> {
|
||||
implements AuthenticationConverter {
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
|
||||
@@ -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<SecurityContext> 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<HttpSecurity> 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;
|
||||
|
||||
@@ -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<MockHttpServletRequest> requestConsumer) throws Exception {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user