Provide configuration for client authentication

Closes gh-380
This commit is contained in:
Joe Grandja
2021-07-29 06:11:41 -04:00
parent 850bd76aee
commit 3d4df8807d
3 changed files with 278 additions and 39 deletions

View File

@@ -29,10 +29,8 @@ import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationProvider;
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;
@@ -42,7 +40,6 @@ import org.springframework.security.oauth2.server.authorization.oidc.web.OidcCli
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
@@ -62,6 +59,7 @@ import org.springframework.util.Assert;
* @author Ovidiu Popa
* @since 0.0.1
* @see AbstractHttpConfigurer
* @see OAuth2ClientAuthenticationConfigurer
* @see OAuth2AuthorizationEndpointConfigurer
* @see OAuth2TokenEndpointConfigurer
* @see RegisteredClientRepository
@@ -72,7 +70,6 @@ import org.springframework.util.Assert;
* @see NimbusJwkSetEndpointFilter
* @see OidcProviderConfigurationEndpointFilter
* @see OAuth2AuthorizationServerMetadataEndpointFilter
* @see OAuth2ClientAuthenticationFilter
* @see OidcClientRegistrationEndpointFilter
*/
public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBuilder<B>>
@@ -103,7 +100,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
*/
public OAuth2AuthorizationServerConfigurer<B> registeredClientRepository(RegisteredClientRepository registeredClientRepository) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
this.getBuilder().setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
getBuilder().setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
return this;
}
@@ -115,7 +112,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
*/
public OAuth2AuthorizationServerConfigurer<B> authorizationService(OAuth2AuthorizationService authorizationService) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
this.getBuilder().setSharedObject(OAuth2AuthorizationService.class, authorizationService);
getBuilder().setSharedObject(OAuth2AuthorizationService.class, authorizationService);
return this;
}
@@ -127,7 +124,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
*/
public OAuth2AuthorizationServerConfigurer<B> authorizationConsentService(OAuth2AuthorizationConsentService authorizationConsentService) {
Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
this.getBuilder().setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
getBuilder().setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
return this;
}
@@ -139,7 +136,18 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
*/
public OAuth2AuthorizationServerConfigurer<B> providerSettings(ProviderSettings providerSettings) {
Assert.notNull(providerSettings, "providerSettings cannot be null");
this.getBuilder().setSharedObject(ProviderSettings.class, providerSettings);
getBuilder().setSharedObject(ProviderSettings.class, providerSettings);
return this;
}
/**
* Configures OAuth 2.0 Client Authentication.
*
* @param clientAuthenticationCustomizer the {@link Customizer} providing access to the {@link OAuth2ClientAuthenticationConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer<B> clientAuthentication(Customizer<OAuth2ClientAuthenticationConfigurer> clientAuthenticationCustomizer) {
clientAuthenticationCustomizer.customize(getConfigurer(OAuth2ClientAuthenticationConfigurer.class));
return this;
}
@@ -182,16 +190,6 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
this.configurers.values().forEach(configurer -> configurer.init(builder));
OAuth2ClientAuthenticationProvider clientAuthenticationProvider =
new OAuth2ClientAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
OAuth2ConfigurerUtils.getAuthorizationService(builder));
PasswordEncoder passwordEncoder = OAuth2ConfigurerUtils.getOptionalBean(builder, PasswordEncoder.class);
if (passwordEncoder != null) {
clientAuthenticationProvider.setPasswordEncoder(passwordEncoder);
}
builder.authenticationProvider(postProcess(clientAuthenticationProvider));
OAuth2TokenIntrospectionAuthenticationProvider tokenIntrospectionAuthenticationProvider =
new OAuth2TokenIntrospectionAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
@@ -245,15 +243,6 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
OAuth2ClientAuthenticationFilter clientAuthenticationFilter =
new OAuth2ClientAuthenticationFilter(
authenticationManager,
new OrRequestMatcher(
getRequestMatcher(OAuth2TokenEndpointConfigurer.class),
this.tokenIntrospectionEndpointMatcher,
this.tokenRevocationEndpointMatcher));
builder.addFilterAfter(postProcess(clientAuthenticationFilter), AbstractPreAuthenticatedProcessingFilter.class);
OAuth2TokenIntrospectionEndpointFilter tokenIntrospectionEndpointFilter =
new OAuth2TokenIntrospectionEndpointFilter(
authenticationManager,
@@ -276,6 +265,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
private Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> createConfigurers() {
Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = new LinkedHashMap<>();
configurers.put(OAuth2ClientAuthenticationConfigurer.class, new OAuth2ClientAuthenticationConfigurer(this::postProcess));
configurers.put(OAuth2AuthorizationEndpointConfigurer.class, new OAuth2AuthorizationEndpointConfigurer(this::postProcess));
configurers.put(OAuth2TokenEndpointConfigurer.class, new OAuth2TokenEndpointConfigurer(this::postProcess));
return configurers;

View File

@@ -0,0 +1,172 @@
/*
* 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 java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
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.core.context.SecurityContext;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
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.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* Configurer for OAuth 2.0 Client Authentication.
*
* @author Joe Grandja
* @since 0.2.0
* @see OAuth2AuthorizationServerConfigurer#clientAuthentication
* @see OAuth2ClientAuthenticationFilter
*/
public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private AuthenticationConverter authenticationConverter;
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
private AuthenticationSuccessHandler authenticationSuccessHandler;
private AuthenticationFailureHandler errorResponseHandler;
/**
* Restrict for internal use only.
*/
OAuth2ClientAuthenticationConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Sets the {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest}
* to an instance of {@link OAuth2ClientAuthenticationToken} used for authenticating the client.
*
* @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest}
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
*/
public OAuth2ClientAuthenticationConfigurer authenticationConverter(AuthenticationConverter authenticationConverter) {
this.authenticationConverter = authenticationConverter;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating an {@link OAuth2ClientAuthenticationToken}.
*
* @param authenticationProvider an {@link AuthenticationProvider} used for authenticating an {@link OAuth2ClientAuthenticationToken}
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
*/
public OAuth2ClientAuthenticationConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling a successful client authentication
* and associating the {@link OAuth2ClientAuthenticationToken} to the {@link SecurityContext}.
*
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used for handling a successful client authentication
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
*/
public OAuth2ClientAuthenticationConfigurer authenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
this.authenticationSuccessHandler = authenticationSuccessHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling a failed client authentication
* and returning the {@link OAuth2Error Error Response}.
*
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling a failed client authentication
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
*/
public OAuth2ClientAuthenticationConfigurer 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 OrRequestMatcher(
new AntPathRequestMatcher(
providerSettings.getTokenEndpoint(),
HttpMethod.POST.name()),
new AntPathRequestMatcher(
providerSettings.getTokenIntrospectionEndpoint(),
HttpMethod.POST.name()),
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);
OAuth2ClientAuthenticationFilter clientAuthenticationFilter = new OAuth2ClientAuthenticationFilter(
authenticationManager, this.requestMatcher);
if (this.authenticationConverter != null) {
clientAuthenticationFilter.setAuthenticationConverter(this.authenticationConverter);
}
if (this.authenticationSuccessHandler != null) {
clientAuthenticationFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
}
if (this.errorResponseHandler != null) {
clientAuthenticationFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
builder.addFilterAfter(postProcess(clientAuthenticationFilter), AbstractPreAuthenticatedProcessingFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private <B extends HttpSecurityBuilder<B>> List<AuthenticationProvider> createDefaultAuthenticationProviders(B builder) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
OAuth2ClientAuthenticationProvider clientAuthenticationProvider =
new OAuth2ClientAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
OAuth2ConfigurerUtils.getAuthorizationService(builder));
PasswordEncoder passwordEncoder = OAuth2ConfigurerUtils.getOptionalBean(builder, PasswordEncoder.class);
if (passwordEncoder != null) {
clientAuthenticationProvider.setPasswordEncoder(passwordEncoder);
}
authenticationProviders.add(clientAuthenticationProvider);
return authenticationProviders;
}
}

View File

@@ -15,12 +15,17 @@
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
@@ -46,6 +51,8 @@ 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.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
@@ -76,6 +83,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -93,10 +101,10 @@ public class OAuth2ClientCredentialsGrantTests {
private static EmbeddedDatabase db;
private static JWKSource<SecurityContext> jwkSource;
private static OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer;
private static AuthenticationConverter accessTokenRequestConverter;
private static AuthenticationConverter authenticationConverter;
private static AuthenticationProvider authenticationProvider;
private static AuthenticationSuccessHandler accessTokenResponseHandler;
private static AuthenticationFailureHandler errorResponseHandler;
private static AuthenticationSuccessHandler authenticationSuccessHandler;
private static AuthenticationFailureHandler authenticationFailureHandler;
@Rule
public final SpringTestRule spring = new SpringTestRule();
@@ -115,10 +123,10 @@ public class OAuth2ClientCredentialsGrantTests {
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
jwtCustomizer = mock(OAuth2TokenCustomizer.class);
accessTokenRequestConverter = mock(AuthenticationConverter.class);
authenticationConverter = mock(AuthenticationConverter.class);
authenticationProvider = mock(AuthenticationProvider.class);
accessTokenResponseHandler = mock(AuthenticationSuccessHandler.class);
errorResponseHandler = mock(AuthenticationFailureHandler.class);
authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class);
authenticationFailureHandler = mock(AuthenticationFailureHandler.class);
db = new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(EmbeddedDatabaseType.HSQL)
@@ -132,6 +140,10 @@ public class OAuth2ClientCredentialsGrantTests {
@Before
public void setup() {
reset(jwtCustomizer);
reset(authenticationConverter);
reset(authenticationProvider);
reset(authenticationSuccessHandler);
reset(authenticationFailureHandler);
}
@After
@@ -202,7 +214,7 @@ public class OAuth2ClientCredentialsGrantTests {
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
OAuth2ClientCredentialsAuthenticationToken clientCredentialsAuthentication =
new OAuth2ClientCredentialsAuthenticationToken(clientPrincipal, null, null);
when(accessTokenRequestConverter.convert(any())).thenReturn(clientCredentialsAuthentication);
when(authenticationConverter.convert(any())).thenReturn(clientCredentialsAuthentication);
OAuth2AccessToken accessToken = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER, "token",
@@ -218,9 +230,32 @@ public class OAuth2ClientCredentialsGrantTests {
registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk());
verify(accessTokenRequestConverter).convert(any());
verify(authenticationConverter).convert(any());
verify(authenticationProvider).authenticate(eq(clientCredentialsAuthentication));
verify(accessTokenResponseHandler).onAuthenticationSuccess(any(), any(), eq(accessTokenAuthentication));
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), eq(accessTokenAuthentication));
}
@Test
public void requestWhenClientAuthenticationCustomizedThenUsed() throws Exception {
this.spring.register(AuthorizationServerConfigurationCustomClientAuthentication.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
this.registeredClientRepository.save(registeredClient);
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
when(authenticationConverter.convert(any())).thenReturn(clientPrincipal);
when(authenticationProvider.supports(eq(OAuth2ClientAuthenticationToken.class))).thenReturn(true);
when(authenticationProvider.authenticate(any())).thenReturn(clientPrincipal);
this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk());
verify(authenticationConverter).convert(any());
verify(authenticationProvider).authenticate(eq(clientPrincipal));
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), eq(clientPrincipal));
}
private static String encodeBasicAuth(String clientId, String secret) throws Exception {
@@ -298,10 +333,10 @@ public class OAuth2ClientCredentialsGrantTests {
authorizationServerConfigurer
.tokenEndpoint(tokenEndpoint ->
tokenEndpoint
.accessTokenRequestConverter(accessTokenRequestConverter)
.accessTokenRequestConverter(authenticationConverter)
.authenticationProvider(authenticationProvider)
.accessTokenResponseHandler(accessTokenResponseHandler)
.errorResponseHandler(errorResponseHandler));
.accessTokenResponseHandler(authenticationSuccessHandler)
.errorResponseHandler(authenticationFailureHandler));
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
http
@@ -316,4 +351,46 @@ public class OAuth2ClientCredentialsGrantTests {
// @formatter:on
}
@EnableWebSecurity
static class AuthorizationServerConfigurationCustomClientAuthentication extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
authenticationSuccessHandler = spy(authenticationSuccessHandler());
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
authorizationServerConfigurer
.clientAuthentication(clientAuthentication ->
clientAuthentication
.authenticationConverter(authenticationConverter)
.authenticationProvider(authenticationProvider)
.authenticationSuccessHandler(authenticationSuccessHandler)
.errorResponseHandler(authenticationFailureHandler));
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer);
return http.build();
}
// @formatter:on
private AuthenticationSuccessHandler authenticationSuccessHandler() {
return new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
org.springframework.security.core.context.SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
SecurityContextHolder.setContext(securityContext);
}
};
}
}
}