diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationEndpointConfigurer.java new file mode 100644 index 00000000..d673976f --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationEndpointConfigurer.java @@ -0,0 +1,209 @@ +/* + * 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.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCodeRequestAuthenticationException; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; +import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter; +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; +import org.springframework.util.StringUtils; + +/** + * Configurer for the OAuth 2.0 Authorization Endpoint. + * + * @author Joe Grandja + * @since 0.1.2 + * @see OAuth2AuthorizationServerConfigurer#authorizationEndpoint + * @see OAuth2AuthorizationEndpointFilter + */ +public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2Configurer { + private RequestMatcher requestMatcher; + private AuthenticationConverter authorizationRequestConverter; + private final List authenticationProviders = new ArrayList<>(); + private AuthenticationSuccessHandler authorizationResponseHandler; + private AuthenticationFailureHandler errorResponseHandler; + private String consentPage; + + /** + * Restrict for internal use only. + */ + OAuth2AuthorizationEndpointConfigurer(ObjectPostProcessor objectPostProcessor) { + super(objectPostProcessor); + } + + /** + * Sets the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest} + * to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the request. + * + * @param authorizationRequestConverter the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest} + * @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration + */ + public OAuth2AuthorizationEndpointConfigurer authorizationRequestConverter(AuthenticationConverter authorizationRequestConverter) { + this.authorizationRequestConverter = authorizationRequestConverter; + return this; + } + + /** + * Adds an {@link AuthenticationProvider} used for authenticating an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}. + * + * @param authenticationProvider an {@link AuthenticationProvider} used for authenticating an {@link OAuth2AuthorizationCodeRequestAuthenticationToken} + * @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration + */ + public OAuth2AuthorizationEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) { + this.authenticationProviders.add(authenticationProvider); + return this; + } + + /** + * Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationToken} + * and returning the {@link OAuth2AuthorizationResponse Authorization Response}. + * + * @param authorizationResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationToken} + * @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration + */ + public OAuth2AuthorizationEndpointConfigurer authorizationResponseHandler(AuthenticationSuccessHandler authorizationResponseHandler) { + this.authorizationResponseHandler = authorizationResponseHandler; + return this; + } + + /** + * Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationException} + * and returning the {@link OAuth2Error Error Response}. + * + * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationException} + * @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration + */ + public OAuth2AuthorizationEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) { + this.errorResponseHandler = errorResponseHandler; + return this; + } + + /** + * Specify the URI to redirect Resource Owners to if consent is required during + * the {@code authorization_code} flow. A default consent page will be generated when + * this attribute is not specified. + * + * If a URI is specified, applications are required to process the specified URI to generate + * a consent page. The query string will contain the following parameters: + * + * + * + * In general, the consent page should create a form that submits + * a request with the following requirements: + * + * + * + * @param consentPage the URI of the custom consent page to redirect to if consent is required (e.g. "/oauth2/consent") + * @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration + */ + public OAuth2AuthorizationEndpointConfigurer consentPage(String consentPage) { + this.consentPage = consentPage; + return this; + } + + @Override + > void init(B builder) { + ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder); + this.requestMatcher = new OrRequestMatcher( + new AntPathRequestMatcher( + providerSettings.authorizationEndpoint(), + HttpMethod.GET.name()), + new AntPathRequestMatcher( + providerSettings.authorizationEndpoint(), + 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); + + OAuth2AuthorizationEndpointFilter authorizationEndpointFilter = + new OAuth2AuthorizationEndpointFilter( + authenticationManager, + providerSettings.authorizationEndpoint()); + if (this.authorizationRequestConverter != null) { + authorizationEndpointFilter.setAuthenticationConverter(this.authorizationRequestConverter); + } + if (this.authorizationResponseHandler != null) { + authorizationEndpointFilter.setAuthenticationSuccessHandler(this.authorizationResponseHandler); + } + if (this.errorResponseHandler != null) { + authorizationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler); + } + if (StringUtils.hasText(this.consentPage)) { + authorizationEndpointFilter.setConsentPage(this.consentPage); + } + builder.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class); + } + + @Override + RequestMatcher getRequestMatcher() { + return this.requestMatcher; + } + + private > List createDefaultAuthenticationProviders(B builder) { + List authenticationProviders = new ArrayList<>(); + + OAuth2AuthorizationCodeRequestAuthenticationProvider authorizationCodeRequestAuthenticationProvider = + new OAuth2AuthorizationCodeRequestAuthenticationProvider( + OAuth2ConfigurerUtils.getRegisteredClientRepository(builder), + OAuth2ConfigurerUtils.getAuthorizationService(builder), + OAuth2ConfigurerUtils.getAuthorizationConsentService(builder)); + authenticationProviders.add(authorizationCodeRequestAuthenticationProvider); + + return authenticationProviders; + } + +} 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 2d9f70b7..564e5924 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 @@ -32,7 +32,6 @@ import org.springframework.security.config.annotation.web.configurers.ExceptionH 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.OAuth2AuthorizationCodeRequestAuthenticationProvider; 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; @@ -42,7 +41,6 @@ import org.springframework.security.oauth2.server.authorization.oidc.authenticat import org.springframework.security.oauth2.server.authorization.oidc.web.OidcClientRegistrationEndpointFilter; 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.OAuth2AuthorizationEndpointFilter; 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; @@ -54,7 +52,6 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** * An {@link AbstractHttpConfigurer} for OAuth 2.0 Authorization Server support. @@ -65,11 +62,11 @@ import org.springframework.util.StringUtils; * @author Ovidiu Popa * @since 0.0.1 * @see AbstractHttpConfigurer + * @see OAuth2AuthorizationEndpointConfigurer * @see OAuth2TokenEndpointConfigurer * @see RegisteredClientRepository * @see OAuth2AuthorizationService * @see OAuth2AuthorizationConsentService - * @see OAuth2AuthorizationEndpointFilter * @see OAuth2TokenIntrospectionEndpointFilter * @see OAuth2TokenRevocationEndpointFilter * @see NimbusJwkSetEndpointFilter @@ -82,7 +79,6 @@ public final class OAuth2AuthorizationServerConfigurer, B> { private final Map, AbstractOAuth2Configurer> configurers = createConfigurers(); - private RequestMatcher authorizationEndpointMatcher; private RequestMatcher tokenIntrospectionEndpointMatcher; private RequestMatcher tokenRevocationEndpointMatcher; private RequestMatcher jwkSetEndpointMatcher; @@ -90,7 +86,7 @@ public final class OAuth2AuthorizationServerConfigurer - this.authorizationEndpointMatcher.matches(request) || + getRequestMatcher(OAuth2AuthorizationEndpointConfigurer.class).matches(request) || getRequestMatcher(OAuth2TokenEndpointConfigurer.class).matches(request) || this.tokenIntrospectionEndpointMatcher.matches(request) || this.tokenRevocationEndpointMatcher.matches(request) || @@ -98,7 +94,6 @@ public final class OAuth2AuthorizationServerConfigurer - *
  • {@code client_id} - the client identifier
  • - *
  • {@code scope} - a space-delimited list of scopes present in the authorization request
  • - *
  • {@code state} - a CSRF protection token
  • - * - * - * In general, the consent page should create a form that submits - * a request with the following requirements: - * - *
      - *
    • It must be an HTTP POST
    • - *
    • It must be submitted to {@link ProviderSettings#authorizationEndpoint()}
    • - *
    • It must include the received {@code client_id} as an HTTP parameter
    • - *
    • It must include the received {@code state} as an HTTP parameter
    • - *
    • It must include the list of {@code scope}s the {@code Resource Owner} - * consented to as an HTTP parameter
    • - *
    - * - * @param consentPage the URI of the custom consent page to redirect to if consent is required (e.g. "/oauth2/consent") + * @param authorizationEndpointCustomizer the {@link Customizer} providing access to the {@link OAuth2AuthorizationEndpointConfigurer} * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration */ - public OAuth2AuthorizationServerConfigurer consentPage(String consentPage) { - this.consentPage = consentPage; + public OAuth2AuthorizationServerConfigurer authorizationEndpoint(Customizer authorizationEndpointCustomizer) { + authorizationEndpointCustomizer.customize(getConfigurer(OAuth2AuthorizationEndpointConfigurer.class)); return this; } @@ -220,13 +192,6 @@ public final class OAuth2AuthorizationServerConfigurer, AbstractOAuth2Configurer> createConfigurers() { Map, AbstractOAuth2Configurer> configurers = new LinkedHashMap<>(); + configurers.put(OAuth2AuthorizationEndpointConfigurer.class, new OAuth2AuthorizationEndpointConfigurer(this::postProcess)); configurers.put(OAuth2TokenEndpointConfigurer.class, new OAuth2TokenEndpointConfigurer(this::postProcess)); return configurers; } @@ -334,13 +291,6 @@ public final class OAuth2AuthorizationServerConfigurer accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); + private static AuthenticationConverter authorizationRequestConverter; + private static AuthenticationProvider authorizationRequestAuthenticationProvider; + private static AuthenticationSuccessHandler authorizationResponseHandler; + private static AuthenticationFailureHandler authorizationErrorResponseHandler; private static String consentPage = "/oauth2/consent"; @Rule @@ -154,6 +170,10 @@ public class OAuth2AuthorizationCodeGrantTests { providerSettings = new ProviderSettings() .authorizationEndpoint("/test/authorize") .tokenEndpoint("/test/token"); + authorizationRequestConverter = mock(AuthenticationConverter.class); + authorizationRequestAuthenticationProvider = mock(AuthenticationProvider.class); + authorizationResponseHandler = mock(AuthenticationSuccessHandler.class); + authorizationErrorResponseHandler = mock(AuthenticationFailureHandler.class); db = new EmbeddedDatabaseBuilder() .generateUniqueName(true) .setType(EmbeddedDatabaseType.HSQL) @@ -469,6 +489,36 @@ public class OAuth2AuthorizationCodeGrantTests { assertThat(authorization).isNotNull(); } + @Test + public void requestWhenAuthorizationEndpointCustomizedThenUsed() throws Exception { + this.spring.register(AuthorizationServerConfigurationCustomAuthorizationEndpoint.class).autowire(); + + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + TestingAuthenticationToken principal = new TestingAuthenticationToken("principalName", "password"); + OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode( + "code", Instant.now(), Instant.now().plus(5, ChronoUnit.MINUTES)); + OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = + OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal) + .authorizationUri("https://provider.com/oauth2/authorize") + .redirectUri(registeredClient.getRedirectUris().iterator().next()) + .scopes(registeredClient.getScopes()) + .state("state") + .authorizationCode(authorizationCode) + .build(); + when(authorizationRequestConverter.convert(any())).thenReturn(authorizationCodeRequestAuthenticationResult); + when(authorizationRequestAuthenticationProvider.supports(eq(OAuth2AuthorizationCodeRequestAuthenticationToken.class))).thenReturn(true); + when(authorizationRequestAuthenticationProvider.authenticate(any())).thenReturn(authorizationCodeRequestAuthenticationResult); + + this.mvc.perform(get(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI) + .params(getAuthorizationRequestParameters(registeredClient)) + .with(user("user"))) + .andExpect(status().isOk()); + + verify(authorizationRequestConverter).convert(any()); + verify(authorizationRequestAuthenticationProvider).authenticate(eq(authorizationCodeRequestAuthenticationResult)); + verify(authorizationResponseHandler).onAuthenticationSuccess(any(), any(), eq(authorizationCodeRequestAuthenticationResult)); + } + private static MultiValueMap getAuthorizationRequestParameters(RegisteredClient registeredClient) { MultiValueMap parameters = new LinkedMultiValueMap<>(); parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue()); @@ -610,7 +660,9 @@ public class OAuth2AuthorizationCodeGrantTests { public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>(); - authorizationServerConfigurer.consentPage(consentPage); + authorizationServerConfigurer + .authorizationEndpoint(authorizationEndpoint -> + authorizationEndpoint.consentPage(consentPage)); RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); http @@ -624,4 +676,33 @@ public class OAuth2AuthorizationCodeGrantTests { } // @formatter:on } + + @EnableWebSecurity + static class AuthorizationServerConfigurationCustomAuthorizationEndpoint extends AuthorizationServerConfiguration { + // @formatter:off + @Bean + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = + new OAuth2AuthorizationServerConfigurer<>(); + authorizationServerConfigurer + .authorizationEndpoint(authorizationEndpoint -> + authorizationEndpoint + .authorizationRequestConverter(authorizationRequestConverter) + .authenticationProvider(authorizationRequestAuthenticationProvider) + .authorizationResponseHandler(authorizationResponseHandler) + .errorResponseHandler(authorizationErrorResponseHandler)); + RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); + + http + .requestMatcher(endpointsMatcher) + .authorizeRequests(authorizeRequests -> + authorizeRequests.anyRequest().authenticated() + ) + .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) + .apply(authorizationServerConfigurer); + return http.build(); + } + // @formatter:on + } + } diff --git a/samples/boot/oauth2-integration/authorizationserver-custom-consent-page/src/main/java/sample/config/AuthorizationServerConfig.java b/samples/boot/oauth2-integration/authorizationserver-custom-consent-page/src/main/java/sample/config/AuthorizationServerConfig.java index f2981971..a5ee5398 100644 --- a/samples/boot/oauth2-integration/authorizationserver-custom-consent-page/src/main/java/sample/config/AuthorizationServerConfig.java +++ b/samples/boot/oauth2-integration/authorizationserver-custom-consent-page/src/main/java/sample/config/AuthorizationServerConfig.java @@ -54,7 +54,10 @@ public class AuthorizationServerConfig { public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>(); - authorizationServerConfigurer.consentPage("/oauth2/consent"); + authorizationServerConfigurer + .authorizationEndpoint(authorizationEndpoint -> + authorizationEndpoint.consentPage("/oauth2/consent")); + RequestMatcher endpointsMatcher = authorizationServerConfigurer .getEndpointsMatcher();