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 355a9ed6..9dac342b 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 @@ -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> @@ -103,7 +100,7 @@ public final class OAuth2AuthorizationServerConfigurer 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 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 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 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 clientAuthentication(Customizer clientAuthenticationCustomizer) { + clientAuthenticationCustomizer.customize(getConfigurer(OAuth2ClientAuthenticationConfigurer.class)); return this; } @@ -182,16 +190,6 @@ public final class OAuth2AuthorizationServerConfigurer 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, AbstractOAuth2Configurer> createConfigurers() { Map, 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; diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientAuthenticationConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientAuthenticationConfigurer.java new file mode 100644 index 00000000..ec4d7187 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientAuthenticationConfigurer.java @@ -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 authenticationProviders = new ArrayList<>(); + private AuthenticationSuccessHandler authenticationSuccessHandler; + private AuthenticationFailureHandler errorResponseHandler; + + /** + * Restrict for internal use only. + */ + OAuth2ClientAuthenticationConfigurer(ObjectPostProcessor 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 + > 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 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); + 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 > List createDefaultAuthenticationProviders(B builder) { + List 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; + } + +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientCredentialsGrantTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientCredentialsGrantTests.java index 577bedcc..6f743a2a 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientCredentialsGrantTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientCredentialsGrantTests.java @@ -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 jwkSource; private static OAuth2TokenCustomizer 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 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); + } + }; + } + + } + }