From 70d433a45aec7128084f5127904e8d070a5e14cd Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Fri, 26 Aug 2022 11:11:17 -0400 Subject: [PATCH 1/9] Update ref-doc with OAuth2Authorization.getAuthorizedScopes() Issue gh-829 --- docs/src/docs/asciidoc/core-model-components.adoc | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/src/docs/asciidoc/core-model-components.adoc b/docs/src/docs/asciidoc/core-model-components.adoc index 6bf23d8a..d3487d15 100644 --- a/docs/src/docs/asciidoc/core-model-components.adoc +++ b/docs/src/docs/asciidoc/core-model-components.adoc @@ -163,8 +163,9 @@ public class OAuth2Authorization implements Serializable { private String registeredClientId; <2> private String principalName; <3> private AuthorizationGrantType authorizationGrantType; <4> - private Map, Token> tokens; <5> - private Map attributes; <6> + private Set authorizedScopes; <5> + private Map, Token> tokens; <6> + private Map attributes; <7> ... @@ -174,8 +175,9 @@ public class OAuth2Authorization implements Serializable { <2> `registeredClientId`: The ID that uniquely identifies the <>. <3> `principalName`: The principal name of the resource owner (or client). <4> `authorizationGrantType`: The `AuthorizationGrantType` used. -<5> `tokens`: The `OAuth2Token` instances (and associated metadata) specific to the executed authorization grant type. -<6> `attributes`: The additional attributes specific to the executed authorization grant type – for example, the authenticated `Principal`, `OAuth2AuthorizationRequest`, authorized scope(s), and others. +<5> `authorizedScopes`: The `Set` of scope(s) authorized for the client. +<6> `tokens`: The `OAuth2Token` instances (and associated metadata) specific to the executed authorization grant type. +<7> `attributes`: The additional attributes specific to the executed authorization grant type – for example, the authenticated `Principal`, `OAuth2AuthorizationRequest`, and others. `OAuth2Authorization` and its associated `OAuth2Token` instances have a set lifespan. A newly issued `OAuth2Token` is active and becomes inactive when it either expires or is invalidated (revoked). From 0994a1e1e14263d106373e3397f8c5c44f9a7708 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Thu, 25 Aug 2022 13:52:15 -0400 Subject: [PATCH 2/9] Allow customizing OIDC Provider Configuration Response Closes gh-616 --- .../src/docs/asciidoc/protocol-endpoints.adoc | 29 ++++- .../web/configurers/OidcConfigurer.java | 51 +++------ ...oviderConfigurationEndpointConfigurer.java | 108 ++++++++++++++++++ ...dcProviderConfigurationEndpointFilter.java | 24 +++- .../annotation/web/configurers/OidcTests.java | 53 +++++++++ ...viderConfigurationEndpointFilterTests.java | 28 ++--- 6 files changed, 241 insertions(+), 52 deletions(-) create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationEndpointConfigurer.java diff --git a/docs/src/docs/asciidoc/protocol-endpoints.adoc b/docs/src/docs/asciidoc/protocol-endpoints.adoc index c0f2f205..06841159 100644 --- a/docs/src/docs/asciidoc/protocol-endpoints.adoc +++ b/docs/src/docs/asciidoc/protocol-endpoints.adoc @@ -198,9 +198,34 @@ The JWK Set endpoint is configured *only* if a `JWKSource` `@Be [[oidc-provider-configuration-endpoint]] == OpenID Connect 1.0 Provider Configuration Endpoint -`OidcConfigurer` provides support for the https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[OpenID Connect 1.0 Provider Configuration endpoint]. +`OidcProviderConfigurationEndpointConfigurer` provides the ability to customize the https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[OpenID Connect 1.0 Provider Configuration endpoint]. +It defines an extension point that lets you customize the https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse[OpenID Provider Configuration response]. -`OidcConfigurer` configures the `OidcProviderConfigurationEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`. +`OidcProviderConfigurationEndpointConfigurer` provides the following configuration option: + +[source,java] +---- +@Bean +public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = + new OAuth2AuthorizationServerConfigurer(); + http.apply(authorizationServerConfigurer); + + authorizationServerConfigurer + .oidc(oidc -> + oidc + .providerConfigurationEndpoint(providerConfigurationEndpoint -> + providerConfigurationEndpoint + .providerConfigurationCustomizer(providerConfigurationCustomizer) <1> + ) + ); + + return http.build(); +} +---- +<1> `providerConfigurationCustomizer()`: The `Consumer` providing access to the `OidcProviderConfiguration.Builder` allowing the ability to customize the claims of the OpenID Provider's configuration. + +`OidcProviderConfigurationEndpointConfigurer` configures the `OidcProviderConfigurationEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`. `OidcProviderConfigurationEndpointFilter` is the `Filter` that returns the https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse[OidcProviderConfiguration response]. [[oidc-user-info-endpoint]] diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java index ad91dee3..9d8b9058 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java @@ -20,13 +20,9 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import org.springframework.http.HttpMethod; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter; -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; @@ -36,9 +32,9 @@ import org.springframework.security.web.util.matcher.RequestMatcher; * @author Joe Grandja * @since 0.2.0 * @see OAuth2AuthorizationServerConfigurer#oidc + * @see OidcProviderConfigurationEndpointConfigurer * @see OidcClientRegistrationEndpointConfigurer * @see OidcUserInfoEndpointConfigurer - * @see OidcProviderConfigurationEndpointFilter */ public final class OidcConfigurer extends AbstractOAuth2Configurer { private final Map, AbstractOAuth2Configurer> configurers = new LinkedHashMap<>(); @@ -49,9 +45,22 @@ public final class OidcConfigurer extends AbstractOAuth2Configurer { */ OidcConfigurer(ObjectPostProcessor objectPostProcessor) { super(objectPostProcessor); + addConfigurer(OidcProviderConfigurationEndpointConfigurer.class, new OidcProviderConfigurationEndpointConfigurer(objectPostProcessor)); addConfigurer(OidcUserInfoEndpointConfigurer.class, new OidcUserInfoEndpointConfigurer(objectPostProcessor)); } + /** + * Configures the OpenID Connect 1.0 Provider Configuration Endpoint. + * + * @param providerConfigurationEndpointCustomizer the {@link Customizer} providing access to the {@link OidcProviderConfigurationEndpointConfigurer} + * @return the {@link OidcConfigurer} for further configuration + * @since 0.4.0 + */ + public OidcConfigurer providerConfigurationEndpoint(Customizer providerConfigurationEndpointCustomizer) { + providerConfigurationEndpointCustomizer.customize(getConfigurer(OidcProviderConfigurationEndpointConfigurer.class)); + return this; + } + /** * Configures the OpenID Connect Dynamic Client Registration 1.0 Endpoint. * @@ -83,39 +92,17 @@ public final class OidcConfigurer extends AbstractOAuth2Configurer { @Override void init(HttpSecurity httpSecurity) { - OidcUserInfoEndpointConfigurer userInfoEndpointConfigurer = - getConfigurer(OidcUserInfoEndpointConfigurer.class); - userInfoEndpointConfigurer.init(httpSecurity); - OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer = - getConfigurer(OidcClientRegistrationEndpointConfigurer.class); - if (clientRegistrationEndpointConfigurer != null) { - clientRegistrationEndpointConfigurer.init(httpSecurity); - } - List requestMatchers = new ArrayList<>(); - requestMatchers.add(new AntPathRequestMatcher( - "/.well-known/openid-configuration", HttpMethod.GET.name())); - requestMatchers.add(userInfoEndpointConfigurer.getRequestMatcher()); - if (clientRegistrationEndpointConfigurer != null) { - requestMatchers.add(clientRegistrationEndpointConfigurer.getRequestMatcher()); - } + this.configurers.values().forEach(configurer -> { + configurer.init(httpSecurity); + requestMatchers.add(configurer.getRequestMatcher()); + }); this.requestMatcher = new OrRequestMatcher(requestMatchers); } @Override void configure(HttpSecurity httpSecurity) { - OidcUserInfoEndpointConfigurer userInfoEndpointConfigurer = - getConfigurer(OidcUserInfoEndpointConfigurer.class); - userInfoEndpointConfigurer.configure(httpSecurity); - OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer = - getConfigurer(OidcClientRegistrationEndpointConfigurer.class); - if (clientRegistrationEndpointConfigurer != null) { - clientRegistrationEndpointConfigurer.configure(httpSecurity); - } - - OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter = - new OidcProviderConfigurationEndpointFilter(); - httpSecurity.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class); + this.configurers.values().forEach(configurer -> configurer.configure(httpSecurity)); } @Override diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationEndpointConfigurer.java new file mode 100644 index 00000000..1141f330 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationEndpointConfigurer.java @@ -0,0 +1,108 @@ +/* + * Copyright 2020-2022 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.oauth2.server.authorization.config.annotation.web.configurers; + +import java.util.function.Consumer; + +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.server.authorization.oidc.OidcProviderConfiguration; +import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter; +import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; + +/** + * Configurer for the OpenID Connect 1.0 Provider Configuration Endpoint. + * + * @author Joe Grandja + * @since 0.4.0 + * @see OidcConfigurer#providerConfigurationEndpoint + * @see OidcProviderConfigurationEndpointFilter + */ +public final class OidcProviderConfigurationEndpointConfigurer extends AbstractOAuth2Configurer { + private RequestMatcher requestMatcher; + private Consumer providerConfigurationCustomizer; + private Consumer defaultProviderConfigurationCustomizer; + + /** + * Restrict for internal use only. + */ + OidcProviderConfigurationEndpointConfigurer(ObjectPostProcessor objectPostProcessor) { + super(objectPostProcessor); + } + + /** + * Sets the {@code Consumer} providing access to the {@link OidcProviderConfiguration.Builder} + * allowing the ability to customize the claims of the OpenID Provider's configuration. + * + * @param providerConfigurationCustomizer the {@code Consumer} providing access to the {@link OidcProviderConfiguration.Builder} + * @return the {@link OidcProviderConfigurationEndpointConfigurer} for further configuration + */ + public OidcProviderConfigurationEndpointConfigurer providerConfigurationCustomizer( + Consumer providerConfigurationCustomizer) { + this.providerConfigurationCustomizer = providerConfigurationCustomizer; + return this; + } + + void addDefaultProviderConfigurationCustomizer( + Consumer defaultProviderConfigurationCustomizer) { + this.defaultProviderConfigurationCustomizer = + this.defaultProviderConfigurationCustomizer == null ? + defaultProviderConfigurationCustomizer : + this.defaultProviderConfigurationCustomizer.andThen(defaultProviderConfigurationCustomizer); + } + + @Override + void init(HttpSecurity httpSecurity) { + this.requestMatcher = new AntPathRequestMatcher( + "/.well-known/openid-configuration", HttpMethod.GET.name()); + } + + @Override + void configure(HttpSecurity httpSecurity) { + OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter = + new OidcProviderConfigurationEndpointFilter(); + Consumer providerConfigurationCustomizer = getProviderConfigurationCustomizer(); + if (providerConfigurationCustomizer != null) { + oidcProviderConfigurationEndpointFilter.setProviderConfigurationCustomizer(providerConfigurationCustomizer); + } + httpSecurity.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class); + } + + private Consumer getProviderConfigurationCustomizer() { + Consumer providerConfigurationCustomizer = null; + if (this.defaultProviderConfigurationCustomizer != null || this.providerConfigurationCustomizer != null) { + if (this.defaultProviderConfigurationCustomizer != null) { + providerConfigurationCustomizer = this.defaultProviderConfigurationCustomizer; + } + if (this.providerConfigurationCustomizer != null) { + providerConfigurationCustomizer = + providerConfigurationCustomizer == null ? + this.providerConfigurationCustomizer : + providerConfigurationCustomizer.andThen(this.providerConfigurationCustomizer); + } + } + return providerConfigurationCustomizer; + } + + @Override + RequestMatcher getRequestMatcher() { + return this.requestMatcher; + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java index 4057079f..3226ec7f 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java @@ -39,6 +39,7 @@ import org.springframework.security.oauth2.server.authorization.oidc.http.conver import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.UriComponentsBuilder; @@ -46,6 +47,7 @@ import org.springframework.web.util.UriComponentsBuilder; * A {@code Filter} that processes OpenID Provider Configuration Requests. * * @author Daniel Garnier-Moiroux + * @author Joe Grandja * @since 0.1.0 * @see OidcProviderConfiguration * @see AuthorizationServerSettings @@ -62,6 +64,19 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques HttpMethod.GET.name()); private final OidcProviderConfigurationHttpMessageConverter providerConfigurationHttpMessageConverter = new OidcProviderConfigurationHttpMessageConverter(); + private Consumer providerConfigurationCustomizer = (providerConfiguration) -> {}; + + /** + * Sets the {@code Consumer} providing access to the {@link OidcProviderConfiguration.Builder} + * allowing the ability to customize the claims of the OpenID Provider's configuration. + * + * @param providerConfigurationCustomizer the {@code Consumer} providing access to the {@link OidcProviderConfiguration.Builder} + * @since 0.4.0 + */ + public void setProviderConfigurationCustomizer(Consumer providerConfigurationCustomizer) { + Assert.notNull(providerConfigurationCustomizer, "providerConfigurationCustomizer cannot be null"); + this.providerConfigurationCustomizer = providerConfigurationCustomizer; + } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) @@ -76,7 +91,7 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques String issuer = authorizationServerContext.getIssuer(); AuthorizationServerSettings authorizationServerSettings = authorizationServerContext.getAuthorizationServerSettings(); - OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.builder() + OidcProviderConfiguration.Builder providerConfiguration = OidcProviderConfiguration.builder() .issuer(issuer) .authorizationEndpoint(asUrl(issuer, authorizationServerSettings.getAuthorizationEndpoint())) .tokenEndpoint(asUrl(issuer, authorizationServerSettings.getTokenEndpoint())) @@ -93,12 +108,13 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques .tokenIntrospectionEndpointAuthenticationMethods(clientAuthenticationMethods()) .subjectType("public") .idTokenSigningAlgorithm(SignatureAlgorithm.RS256.getName()) - .scope(OidcScopes.OPENID) - .build(); + .scope(OidcScopes.OPENID); + + this.providerConfigurationCustomizer.accept(providerConfiguration); ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); this.providerConfigurationHttpMessageConverter.write( - providerConfiguration, MediaType.APPLICATION_JSON, httpResponse); + providerConfiguration.build(), MediaType.APPLICATION_JSON, httpResponse); } private static Consumer> clientAuthenticationMethods() { diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java index a0dc9e45..cf530093 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java @@ -24,6 +24,7 @@ import java.util.Base64; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.Consumer; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.source.JWKSource; @@ -36,6 +37,7 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -69,6 +71,7 @@ import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadataClaimNames; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations; @@ -79,6 +82,7 @@ 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.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.oauth2.server.authorization.jackson2.TestingAuthenticationTokenMixin; +import org.springframework.security.oauth2.server.authorization.oidc.OidcProviderConfiguration; import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.oauth2.server.authorization.test.SpringTestRule; import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator; @@ -101,6 +105,7 @@ import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.hasItems; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -196,6 +201,17 @@ public class OidcTests { .andExpect(status().is2xxSuccessful()); } + // gh-616 + @Test + public void requestWhenConfigurationRequestAndConfigurationCustomizerSetThenReturnCustomConfigurationResponse() throws Exception { + this.spring.register(AuthorizationServerConfigurationWithProviderConfigurationCustomizer.class).autowire(); + + this.mvc.perform(get(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, + hasItems(OidcScopes.OPENID, OidcScopes.PROFILE, OidcScopes.EMAIL))); + } + @Test public void loadContextWhenIssuerNotValidUrlThenThrowException() { assertThatThrownBy( @@ -464,6 +480,43 @@ public class OidcTests { } + @Configuration + @EnableWebSecurity + static class AuthorizationServerConfigurationWithProviderConfigurationCustomizer extends AuthorizationServerConfiguration { + + // @formatter:off + @Bean + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = + new OAuth2AuthorizationServerConfigurer(); + http.apply(authorizationServerConfigurer); + + authorizationServerConfigurer + .oidc(oidc -> + oidc.providerConfigurationEndpoint(providerConfigurationEndpoint -> + providerConfigurationEndpoint + .providerConfigurationCustomizer(providerConfigurationCustomizer()))); + + RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); + + http + .requestMatcher(endpointsMatcher) + .authorizeRequests(authorizeRequests -> + authorizeRequests.anyRequest().authenticated() + ) + .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)); + + return http.build(); + } + // @formatter:on + + private Consumer providerConfigurationCustomizer() { + return (providerConfiguration) -> + providerConfiguration.scope(OidcScopes.PROFILE).scope(OidcScopes.EMAIL); + } + + } + @EnableWebSecurity @Import(OAuth2AuthorizationServerConfiguration.class) static class AuthorizationServerConfigurationWithIssuer extends AuthorizationServerConfiguration { diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java index 029a1d3a..52530eb1 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java @@ -31,6 +31,7 @@ import org.springframework.security.oauth2.server.authorization.settings.Authori import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -40,9 +41,11 @@ import static org.mockito.Mockito.verifyNoInteractions; * Tests for {@link OidcProviderConfigurationEndpointFilter}. * * @author Daniel Garnier-Moiroux + * @author Joe Grandja */ public class OidcProviderConfigurationEndpointFilterTests { private static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/.well-known/openid-configuration"; + private final OidcProviderConfigurationEndpointFilter filter = new OidcProviderConfigurationEndpointFilter(); @After public void cleanup() { @@ -50,35 +53,34 @@ public class OidcProviderConfigurationEndpointFilterTests { } @Test - public void doFilterWhenNotConfigurationRequestThenNotProcessed() throws Exception { - AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder().build(); - AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null)); - OidcProviderConfigurationEndpointFilter filter = new OidcProviderConfigurationEndpointFilter(); + public void setProviderConfigurationCustomizerWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.filter.setProviderConfigurationCustomizer(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("providerConfigurationCustomizer cannot be null"); + } + @Test + public void doFilterWhenNotConfigurationRequestThenNotProcessed() throws Exception { String requestUri = "/path"; MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); request.setServletPath(requestUri); MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain filterChain = mock(FilterChain.class); - filter.doFilter(request, response, filterChain); + this.filter.doFilter(request, response, filterChain); verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); } @Test public void doFilterWhenConfigurationRequestPostThenNotProcessed() throws Exception { - AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder().build(); - AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null)); - OidcProviderConfigurationEndpointFilter filter = new OidcProviderConfigurationEndpointFilter(); - String requestUri = DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI; MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri); request.setServletPath(requestUri); MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain filterChain = mock(FilterChain.class); - filter.doFilter(request, response, filterChain); + this.filter.doFilter(request, response, filterChain); verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); } @@ -103,7 +105,6 @@ public class OidcProviderConfigurationEndpointFilterTests { .tokenIntrospectionEndpoint(tokenIntrospectionEndpoint) .build(); AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null)); - OidcProviderConfigurationEndpointFilter filter = new OidcProviderConfigurationEndpointFilter(); String requestUri = DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI; MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); @@ -111,7 +112,7 @@ public class OidcProviderConfigurationEndpointFilterTests { MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain filterChain = mock(FilterChain.class); - filter.doFilter(request, response, filterChain); + this.filter.doFilter(request, response, filterChain); verifyNoInteractions(filterChain); @@ -140,7 +141,6 @@ public class OidcProviderConfigurationEndpointFilterTests { .issuer("https://this is an invalid URL") .build(); AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null)); - OidcProviderConfigurationEndpointFilter filter = new OidcProviderConfigurationEndpointFilter(); String requestUri = DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI; MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); @@ -149,7 +149,7 @@ public class OidcProviderConfigurationEndpointFilterTests { FilterChain filterChain = mock(FilterChain.class); assertThatIllegalArgumentException() - .isThrownBy(() -> filter.doFilter(request, response, filterChain)) + .isThrownBy(() -> this.filter.doFilter(request, response, filterChain)) .withMessage("issuer must be a valid URL"); } From 8043b8c9492106cede23c97b347b027b375300d0 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 29 Aug 2022 14:17:48 -0400 Subject: [PATCH 3/9] Allow customizing Authorization Server Metadata Response Closes gh-878 --- .../src/docs/asciidoc/protocol-endpoints.adoc | 27 ++++- .../OAuth2AuthorizationServerConfigurer.java | 48 ++++---- ...ationServerMetadataEndpointConfigurer.java | 108 ++++++++++++++++++ ...orizationServerMetadataEndpointFilter.java | 24 +++- ...Auth2AuthorizationServerMetadataTests.java | 56 +++++++++ ...tionServerMetadataEndpointFilterTests.java | 32 +++--- 6 files changed, 247 insertions(+), 48 deletions(-) create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataEndpointConfigurer.java diff --git a/docs/src/docs/asciidoc/protocol-endpoints.adoc b/docs/src/docs/asciidoc/protocol-endpoints.adoc index 06841159..45b472e8 100644 --- a/docs/src/docs/asciidoc/protocol-endpoints.adoc +++ b/docs/src/docs/asciidoc/protocol-endpoints.adoc @@ -179,10 +179,31 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h [[oauth2-authorization-server-metadata-endpoint]] == OAuth2 Authorization Server Metadata Endpoint -`OAuth2AuthorizationServerConfigurer` provides support for the https://datatracker.ietf.org/doc/html/rfc8414#section-3[OAuth2 Authorization Server Metadata endpoint]. +`OAuth2AuthorizationServerMetadataEndpointConfigurer` provides the ability to customize the https://datatracker.ietf.org/doc/html/rfc8414#section-3[OAuth2 Authorization Server Metadata endpoint]. +It defines an extension point that lets you customize the https://datatracker.ietf.org/doc/html/rfc8414#section-3.2[OAuth2 Authorization Server Metadata response]. -`OAuth2AuthorizationServerConfigurer` configures the `OAuth2AuthorizationServerMetadataEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`. -`OAuth2AuthorizationServerMetadataEndpointFilter` is the `Filter` that processes https://datatracker.ietf.org/doc/html/rfc8414#section-3.1[OAuth2 authorization server metadata requests] and returns the https://datatracker.ietf.org/doc/html/rfc8414#section-3.2[OAuth2AuthorizationServerMetadata response]. +`OAuth2AuthorizationServerMetadataEndpointConfigurer` provides the following configuration option: + +[source,java] +---- +@Bean +public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = + new OAuth2AuthorizationServerConfigurer(); + http.apply(authorizationServerConfigurer); + + authorizationServerConfigurer + .authorizationServerMetadataEndpoint(authorizationServerMetadataEndpoint -> + authorizationServerMetadataEndpoint + .authorizationServerMetadataCustomizer(authorizationServerMetadataCustomizer)); <1> + + return http.build(); +} +---- +<1> `authorizationServerMetadataCustomizer()`: The `Consumer` providing access to the `OAuth2AuthorizationServerMetadata.Builder` allowing the ability to customize the claims of the Authorization Server's configuration. + +`OAuth2AuthorizationServerMetadataEndpointConfigurer` configures the `OAuth2AuthorizationServerMetadataEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`. +`OAuth2AuthorizationServerMetadataEndpointFilter` is the `Filter` that returns the https://datatracker.ietf.org/doc/html/rfc8414#section-3.2[OAuth2AuthorizationServerMetadata response]. [[jwk-set-endpoint]] == JWK Set Endpoint diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java index 28919e88..8e9f6332 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java @@ -34,7 +34,6 @@ import org.springframework.security.oauth2.server.authorization.client.Registere import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter; -import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter; import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; import org.springframework.security.web.context.SecurityContextHolderFilter; @@ -54,6 +53,7 @@ import org.springframework.util.Assert; * @since 0.0.1 * @see AbstractHttpConfigurer * @see OAuth2ClientAuthenticationConfigurer + * @see OAuth2AuthorizationServerMetadataEndpointConfigurer * @see OAuth2AuthorizationEndpointConfigurer * @see OAuth2TokenEndpointConfigurer * @see OAuth2TokenIntrospectionEndpointConfigurer @@ -63,22 +63,20 @@ import org.springframework.util.Assert; * @see OAuth2AuthorizationService * @see OAuth2AuthorizationConsentService * @see NimbusJwkSetEndpointFilter - * @see OAuth2AuthorizationServerMetadataEndpointFilter */ public final class OAuth2AuthorizationServerConfigurer extends AbstractHttpConfigurer { private final Map, AbstractOAuth2Configurer> configurers = createConfigurers(); - private RequestMatcher jwkSetEndpointMatcher; - private RequestMatcher authorizationServerMetadataEndpointMatcher; private final RequestMatcher endpointsMatcher = (request) -> - getRequestMatcher(OAuth2AuthorizationEndpointConfigurer.class).matches(request) || - getRequestMatcher(OAuth2TokenEndpointConfigurer.class).matches(request) || - getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class).matches(request) || - getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class).matches(request) || - getRequestMatcher(OidcConfigurer.class).matches(request) || - this.jwkSetEndpointMatcher.matches(request) || - this.authorizationServerMetadataEndpointMatcher.matches(request); + getRequestMatcher(OAuth2AuthorizationServerMetadataEndpointConfigurer.class).matches(request) || + getRequestMatcher(OAuth2AuthorizationEndpointConfigurer.class).matches(request) || + getRequestMatcher(OAuth2TokenEndpointConfigurer.class).matches(request) || + getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class).matches(request) || + getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class).matches(request) || + getRequestMatcher(OidcConfigurer.class).matches(request) || + this.jwkSetEndpointMatcher.matches(request); + private RequestMatcher jwkSetEndpointMatcher; /** * Sets the repository of registered clients. @@ -152,6 +150,18 @@ public final class OAuth2AuthorizationServerConfigurer return this; } + /** + * Configures the OAuth 2.0 Authorization Server Metadata Endpoint. + * + * @param authorizationServerMetadataEndpointCustomizer the {@link Customizer} providing access to the {@link OAuth2AuthorizationServerMetadataEndpointConfigurer} + * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration + * @since 0.4.0 + */ + public OAuth2AuthorizationServerConfigurer authorizationServerMetadataEndpoint(Customizer authorizationServerMetadataEndpointCustomizer) { + authorizationServerMetadataEndpointCustomizer.customize(getConfigurer(OAuth2AuthorizationServerMetadataEndpointConfigurer.class)); + return this; + } + /** * Configures the OAuth 2.0 Authorization Endpoint. * @@ -222,7 +232,9 @@ public final class OAuth2AuthorizationServerConfigurer public void init(HttpSecurity httpSecurity) { AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity); validateAuthorizationServerSettings(authorizationServerSettings); - initEndpointMatchers(authorizationServerSettings); + + this.jwkSetEndpointMatcher = new AntPathRequestMatcher( + authorizationServerSettings.getJwkSetEndpoint(), HttpMethod.GET.name()); this.configurers.values().forEach(configurer -> configurer.init(httpSecurity)); @@ -253,15 +265,12 @@ public final class OAuth2AuthorizationServerConfigurer jwkSource, authorizationServerSettings.getJwkSetEndpoint()); httpSecurity.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class); } - - OAuth2AuthorizationServerMetadataEndpointFilter authorizationServerMetadataEndpointFilter = - new OAuth2AuthorizationServerMetadataEndpointFilter(); - httpSecurity.addFilterBefore(postProcess(authorizationServerMetadataEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class); } private Map, AbstractOAuth2Configurer> createConfigurers() { Map, AbstractOAuth2Configurer> configurers = new LinkedHashMap<>(); configurers.put(OAuth2ClientAuthenticationConfigurer.class, new OAuth2ClientAuthenticationConfigurer(this::postProcess)); + configurers.put(OAuth2AuthorizationServerMetadataEndpointConfigurer.class, new OAuth2AuthorizationServerMetadataEndpointConfigurer(this::postProcess)); configurers.put(OAuth2AuthorizationEndpointConfigurer.class, new OAuth2AuthorizationEndpointConfigurer(this::postProcess)); configurers.put(OAuth2TokenEndpointConfigurer.class, new OAuth2TokenEndpointConfigurer(this::postProcess)); configurers.put(OAuth2TokenIntrospectionEndpointConfigurer.class, new OAuth2TokenIntrospectionEndpointConfigurer(this::postProcess)); @@ -279,13 +288,6 @@ public final class OAuth2AuthorizationServerConfigurer return getConfigurer(configurerType).getRequestMatcher(); } - private void initEndpointMatchers(AuthorizationServerSettings authorizationServerSettings) { - this.jwkSetEndpointMatcher = new AntPathRequestMatcher( - authorizationServerSettings.getJwkSetEndpoint(), HttpMethod.GET.name()); - this.authorizationServerMetadataEndpointMatcher = new AntPathRequestMatcher( - "/.well-known/oauth-authorization-server", HttpMethod.GET.name()); - } - private static void validateAuthorizationServerSettings(AuthorizationServerSettings authorizationServerSettings) { if (authorizationServerSettings.getIssuer() != null) { URI issuerUri; diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataEndpointConfigurer.java new file mode 100644 index 00000000..25c4fb39 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataEndpointConfigurer.java @@ -0,0 +1,108 @@ +/* + * Copyright 2020-2022 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.oauth2.server.authorization.config.annotation.web.configurers; + +import java.util.function.Consumer; + +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata; +import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter; +import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; + +/** + * Configurer for the OAuth 2.0 Authorization Server Metadata Endpoint. + * + * @author Joe Grandja + * @since 0.4.0 + * @see OAuth2AuthorizationServerConfigurer#authorizationServerMetadataEndpoint + * @see OAuth2AuthorizationServerMetadataEndpointFilter + */ +public final class OAuth2AuthorizationServerMetadataEndpointConfigurer extends AbstractOAuth2Configurer { + private RequestMatcher requestMatcher; + private Consumer authorizationServerMetadataCustomizer; + private Consumer defaultAuthorizationServerMetadataCustomizer; + + /** + * Restrict for internal use only. + */ + OAuth2AuthorizationServerMetadataEndpointConfigurer(ObjectPostProcessor objectPostProcessor) { + super(objectPostProcessor); + } + + /** + * Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationServerMetadata.Builder} + * allowing the ability to customize the claims of the Authorization Server's configuration. + * + * @param authorizationServerMetadataCustomizer the {@code Consumer} providing access to the {@link OAuth2AuthorizationServerMetadata.Builder} + * @return the {@link OAuth2AuthorizationServerMetadataEndpointConfigurer} for further configuration + */ + public OAuth2AuthorizationServerMetadataEndpointConfigurer authorizationServerMetadataCustomizer( + Consumer authorizationServerMetadataCustomizer) { + this.authorizationServerMetadataCustomizer = authorizationServerMetadataCustomizer; + return this; + } + + void addDefaultAuthorizationServerMetadataCustomizer( + Consumer defaultAuthorizationServerMetadataCustomizer) { + this.defaultAuthorizationServerMetadataCustomizer = + this.defaultAuthorizationServerMetadataCustomizer == null ? + defaultAuthorizationServerMetadataCustomizer : + this.defaultAuthorizationServerMetadataCustomizer.andThen(defaultAuthorizationServerMetadataCustomizer); + } + + @Override + void init(HttpSecurity httpSecurity) { + this.requestMatcher = new AntPathRequestMatcher( + "/.well-known/oauth-authorization-server", HttpMethod.GET.name()); + } + + @Override + void configure(HttpSecurity httpSecurity) { + OAuth2AuthorizationServerMetadataEndpointFilter authorizationServerMetadataEndpointFilter = + new OAuth2AuthorizationServerMetadataEndpointFilter(); + Consumer authorizationServerMetadataCustomizer = getAuthorizationServerMetadataCustomizer(); + if (authorizationServerMetadataCustomizer != null) { + authorizationServerMetadataEndpointFilter.setAuthorizationServerMetadataCustomizer(authorizationServerMetadataCustomizer); + } + httpSecurity.addFilterBefore(postProcess(authorizationServerMetadataEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class); + } + + private Consumer getAuthorizationServerMetadataCustomizer() { + Consumer authorizationServerMetadataCustomizer = null; + if (this.defaultAuthorizationServerMetadataCustomizer != null || this.authorizationServerMetadataCustomizer != null) { + if (this.defaultAuthorizationServerMetadataCustomizer != null) { + authorizationServerMetadataCustomizer = this.defaultAuthorizationServerMetadataCustomizer; + } + if (this.authorizationServerMetadataCustomizer != null) { + authorizationServerMetadataCustomizer = + authorizationServerMetadataCustomizer == null ? + this.authorizationServerMetadataCustomizer : + authorizationServerMetadataCustomizer.andThen(this.authorizationServerMetadataCustomizer); + } + } + return authorizationServerMetadataCustomizer; + } + + @Override + RequestMatcher getRequestMatcher() { + return this.requestMatcher; + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java index 6823b026..c988d56e 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java @@ -37,6 +37,7 @@ import org.springframework.security.oauth2.server.authorization.http.converter.O import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.UriComponentsBuilder; @@ -44,6 +45,7 @@ import org.springframework.web.util.UriComponentsBuilder; * A {@code Filter} that processes OAuth 2.0 Authorization Server Metadata Requests. * * @author Daniel Garnier-Moiroux + * @author Joe Grandja * @since 0.1.1 * @see OAuth2AuthorizationServerMetadata * @see AuthorizationServerSettings @@ -60,6 +62,19 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP HttpMethod.GET.name()); private final OAuth2AuthorizationServerMetadataHttpMessageConverter authorizationServerMetadataHttpMessageConverter = new OAuth2AuthorizationServerMetadataHttpMessageConverter(); + private Consumer authorizationServerMetadataCustomizer = (authorizationServerMetadata) -> {}; + + /** + * Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationServerMetadata.Builder} + * allowing the ability to customize the claims of the Authorization Server's configuration. + * + * @param authorizationServerMetadataCustomizer the {@code Consumer} providing access to the {@link OAuth2AuthorizationServerMetadata.Builder} + * @since 0.4.0 + */ + public void setAuthorizationServerMetadataCustomizer(Consumer authorizationServerMetadataCustomizer) { + Assert.notNull(authorizationServerMetadataCustomizer, "authorizationServerMetadataCustomizer cannot be null"); + this.authorizationServerMetadataCustomizer = authorizationServerMetadataCustomizer; + } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) @@ -74,7 +89,7 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP String issuer = authorizationServerContext.getIssuer(); AuthorizationServerSettings authorizationServerSettings = authorizationServerContext.getAuthorizationServerSettings(); - OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.builder() + OAuth2AuthorizationServerMetadata.Builder authorizationServerMetadata = OAuth2AuthorizationServerMetadata.builder() .issuer(issuer) .authorizationEndpoint(asUrl(issuer, authorizationServerSettings.getAuthorizationEndpoint())) .tokenEndpoint(asUrl(issuer, authorizationServerSettings.getTokenEndpoint())) @@ -88,12 +103,13 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP .tokenRevocationEndpointAuthenticationMethods(clientAuthenticationMethods()) .tokenIntrospectionEndpoint(asUrl(issuer, authorizationServerSettings.getTokenIntrospectionEndpoint())) .tokenIntrospectionEndpointAuthenticationMethods(clientAuthenticationMethods()) - .codeChallengeMethod("S256") - .build(); + .codeChallengeMethod("S256"); + + this.authorizationServerMetadataCustomizer.accept(authorizationServerMetadata); ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); this.authorizationServerMetadataHttpMessageConverter.write( - authorizationServerMetadata, MediaType.APPLICATION_JSON, httpResponse); + authorizationServerMetadata.build(), MediaType.APPLICATION_JSON, httpResponse); } private static Consumer> clientAuthenticationMethods() { diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataTests.java index 7750a77d..5f2449e4 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataTests.java @@ -15,6 +15,8 @@ */ package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers; +import java.util.function.Consumer; + import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.SecurityContext; @@ -26,14 +28,18 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; 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.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.oauth2.jose.TestJwks; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadataClaimNames; import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; @@ -41,8 +47,11 @@ import org.springframework.security.oauth2.server.authorization.client.TestRegis import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.oauth2.server.authorization.test.SpringTestRule; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.test.web.servlet.MockMvc; +import static org.hamcrest.CoreMatchers.hasItems; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -111,6 +120,17 @@ public class OAuth2AuthorizationServerMetadataTests { .andReturn(); } + // gh-616 + @Test + public void requestWhenAuthorizationServerMetadataRequestAndMetadataCustomizerSetThenReturnCustomMetadataResponse() throws Exception { + this.spring.register(AuthorizationServerConfigurationWithMetadataCustomizer.class).autowire(); + + this.mvc.perform(get(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI)) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, + hasItems("scope1", "scope2"))); + } + @EnableWebSecurity @Import(OAuth2AuthorizationServerConfiguration.class) static class AuthorizationServerConfiguration { @@ -139,6 +159,42 @@ public class OAuth2AuthorizationServerMetadataTests { } } + @Configuration + @EnableWebSecurity + static class AuthorizationServerConfigurationWithMetadataCustomizer extends AuthorizationServerConfiguration { + + // @formatter:off + @Bean + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = + new OAuth2AuthorizationServerConfigurer(); + http.apply(authorizationServerConfigurer); + + authorizationServerConfigurer + .authorizationServerMetadataEndpoint(authorizationServerMetadataEndpoint -> + authorizationServerMetadataEndpoint + .authorizationServerMetadataCustomizer(authorizationServerMetadataCustomizer())); + + RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); + + http + .requestMatcher(endpointsMatcher) + .authorizeRequests(authorizeRequests -> + authorizeRequests.anyRequest().authenticated() + ) + .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)); + + return http.build(); + } + // @formatter:on + + private Consumer authorizationServerMetadataCustomizer() { + return (authorizationServerMetadata) -> + authorizationServerMetadata.scope("scope1").scope("scope2"); + } + + } + @EnableWebSecurity @Import(OAuth2AuthorizationServerConfiguration.class) static class AuthorizationServerConfigurationWithIssuerNotSet extends AuthorizationServerConfiguration { diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java index 43bb4edd..ff73c5aa 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java @@ -31,6 +31,7 @@ import org.springframework.security.oauth2.server.authorization.settings.Authori import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -40,9 +41,11 @@ import static org.mockito.Mockito.verifyNoInteractions; * Tests for {@link OAuth2AuthorizationServerMetadataEndpointFilter}. * * @author Daniel Garnier-Moiroux + * @author Joe Grandja */ public class OAuth2AuthorizationServerMetadataEndpointFilterTests { private static final String DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI = "/.well-known/oauth-authorization-server"; + private final OAuth2AuthorizationServerMetadataEndpointFilter filter = new OAuth2AuthorizationServerMetadataEndpointFilter(); @After public void cleanup() { @@ -50,39 +53,34 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests { } @Test - public void doFilterWhenNotAuthorizationServerMetadataRequestThenNotProcessed() throws Exception { - AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder() - .issuer("https://example.com") - .build(); - AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null)); - OAuth2AuthorizationServerMetadataEndpointFilter filter = new OAuth2AuthorizationServerMetadataEndpointFilter(); + public void setAuthorizationServerMetadataCustomizerWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.filter.setAuthorizationServerMetadataCustomizer(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("authorizationServerMetadataCustomizer cannot be null"); + } + @Test + public void doFilterWhenNotAuthorizationServerMetadataRequestThenNotProcessed() throws Exception { String requestUri = "/path"; MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); request.setServletPath(requestUri); MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain filterChain = mock(FilterChain.class); - filter.doFilter(request, response, filterChain); + this.filter.doFilter(request, response, filterChain); verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); } @Test public void doFilterWhenAuthorizationServerMetadataRequestPostThenNotProcessed() throws Exception { - AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder() - .issuer("https://example.com") - .build(); - AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null)); - OAuth2AuthorizationServerMetadataEndpointFilter filter = new OAuth2AuthorizationServerMetadataEndpointFilter(); - String requestUri = DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI; MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri); request.setServletPath(requestUri); MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain filterChain = mock(FilterChain.class); - filter.doFilter(request, response, filterChain); + this.filter.doFilter(request, response, filterChain); verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); } @@ -105,7 +103,6 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests { .tokenIntrospectionEndpoint(tokenIntrospectionEndpoint) .build(); AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null)); - OAuth2AuthorizationServerMetadataEndpointFilter filter = new OAuth2AuthorizationServerMetadataEndpointFilter(); String requestUri = DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI; MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); @@ -113,7 +110,7 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests { MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain filterChain = mock(FilterChain.class); - filter.doFilter(request, response, filterChain); + this.filter.doFilter(request, response, filterChain); verifyNoInteractions(filterChain); @@ -139,7 +136,6 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests { .issuer("https://this is an invalid URL") .build(); AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null)); - OAuth2AuthorizationServerMetadataEndpointFilter filter = new OAuth2AuthorizationServerMetadataEndpointFilter(); String requestUri = DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI; MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); @@ -149,7 +145,7 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests { assertThatIllegalArgumentException() - .isThrownBy(() -> filter.doFilter(request, response, filterChain)) + .isThrownBy(() -> this.filter.doFilter(request, response, filterChain)) .withMessage("issuer must be a valid URL"); } From 4466cbe69d9ce06d042a8cd1aecc1eb4931505c9 Mon Sep 17 00:00:00 2001 From: 721806280 <33091348+721806280@users.noreply.github.com> Date: Mon, 20 Jun 2022 15:51:08 +0800 Subject: [PATCH 4/9] Use configured ID Token signature algorithm Closes gh-787 --- .../oauth2/server/authorization/token/JwtGenerator.java | 9 +++++---- .../server/authorization/token/JwtGeneratorTests.java | 9 ++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java index 318cf6a6..b48f6694 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java @@ -89,11 +89,14 @@ public final class JwtGenerator implements OAuth2TokenGenerator { Instant issuedAt = Instant.now(); Instant expiresAt; + JwsHeader.Builder headersBuilder; if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) { // TODO Allow configuration for ID Token time-to-live expiresAt = issuedAt.plus(30, ChronoUnit.MINUTES); + headersBuilder = JwsHeader.with(registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm()); } else { expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive()); + headersBuilder = JwsHeader.with(SignatureAlgorithm.RS256); } // @formatter:off @@ -125,11 +128,9 @@ public final class JwtGenerator implements OAuth2TokenGenerator { } // @formatter:on - JwsHeader.Builder jwsHeaderBuilder = JwsHeader.with(SignatureAlgorithm.RS256); - if (this.jwtCustomizer != null) { // @formatter:off - JwtEncodingContext.Builder jwtContextBuilder = JwtEncodingContext.with(jwsHeaderBuilder, claimsBuilder) + JwtEncodingContext.Builder jwtContextBuilder = JwtEncodingContext.with(headersBuilder, claimsBuilder) .registeredClient(context.getRegisteredClient()) .principal(context.getPrincipal()) .authorizationServerContext(context.getAuthorizationServerContext()) @@ -148,7 +149,7 @@ public final class JwtGenerator implements OAuth2TokenGenerator { this.jwtCustomizer.customize(jwtContext); } - JwsHeader jwsHeader = jwsHeaderBuilder.build(); + JwsHeader jwsHeader = headersBuilder.build(); JwtClaimsSet claims = claimsBuilder.build(); Jwt jwt = this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims)); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java index b4f672cf..8534ee60 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java @@ -201,9 +201,6 @@ public class JwtGeneratorTests { ArgumentCaptor jwtEncoderParametersCaptor = ArgumentCaptor.forClass(JwtEncoderParameters.class); verify(this.jwtEncoder).encode(jwtEncoderParametersCaptor.capture()); - JwsHeader jwsHeader = jwtEncoderParametersCaptor.getValue().getJwsHeader(); - assertThat(jwsHeader.getAlgorithm()).isEqualTo(SignatureAlgorithm.RS256); - JwtClaimsSet jwtClaimsSet = jwtEncoderParametersCaptor.getValue().getClaims(); assertThat(jwtClaimsSet.getIssuer().toExternalForm()).isEqualTo(tokenContext.getAuthorizationServerContext().getIssuer()); assertThat(jwtClaimsSet.getSubject()).isEqualTo(tokenContext.getAuthorization().getPrincipalName()); @@ -211,14 +208,20 @@ public class JwtGeneratorTests { Instant issuedAt = Instant.now(); Instant expiresAt; + JwsHeader.Builder headersBuilder; if (tokenContext.getTokenType().equals(OAuth2TokenType.ACCESS_TOKEN)) { expiresAt = issuedAt.plus(tokenContext.getRegisteredClient().getTokenSettings().getAccessTokenTimeToLive()); + headersBuilder = JwsHeader.with(SignatureAlgorithm.RS256); } else { expiresAt = issuedAt.plus(30, ChronoUnit.MINUTES); + headersBuilder = JwsHeader.with(tokenContext.getRegisteredClient().getTokenSettings().getIdTokenSignatureAlgorithm()); } assertThat(jwtClaimsSet.getIssuedAt()).isBetween(issuedAt.minusSeconds(1), issuedAt.plusSeconds(1)); assertThat(jwtClaimsSet.getExpiresAt()).isBetween(expiresAt.minusSeconds(1), expiresAt.plusSeconds(1)); + JwsHeader jwsHeader = jwtEncoderParametersCaptor.getValue().getJwsHeader(); + assertThat(jwsHeader.getAlgorithm()).isEqualTo(headersBuilder.build().getAlgorithm()); + if (tokenContext.getTokenType().equals(OAuth2TokenType.ACCESS_TOKEN)) { assertThat(jwtClaimsSet.getNotBefore()).isBetween(issuedAt.minusSeconds(1), issuedAt.plusSeconds(1)); From 502fa24cfb0a015184678157ac449b38e8296883 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 30 Aug 2022 05:23:11 -0400 Subject: [PATCH 5/9] Polish gh-787 --- .../authorization/token/JwtGenerator.java | 14 +++++++++----- .../authorization/token/JwtGeneratorTests.java | 18 +++++++++++------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java index b48f6694..3cc52de7 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java @@ -27,6 +27,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.JwsHeader; import org.springframework.security.oauth2.jwt.Jwt; @@ -89,14 +90,15 @@ public final class JwtGenerator implements OAuth2TokenGenerator { Instant issuedAt = Instant.now(); Instant expiresAt; - JwsHeader.Builder headersBuilder; + JwsAlgorithm jwsAlgorithm = SignatureAlgorithm.RS256; if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) { // TODO Allow configuration for ID Token time-to-live expiresAt = issuedAt.plus(30, ChronoUnit.MINUTES); - headersBuilder = JwsHeader.with(registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm()); + if (registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm() != null) { + jwsAlgorithm = registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm(); + } } else { expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive()); - headersBuilder = JwsHeader.with(SignatureAlgorithm.RS256); } // @formatter:off @@ -128,9 +130,11 @@ public final class JwtGenerator implements OAuth2TokenGenerator { } // @formatter:on + JwsHeader.Builder jwsHeaderBuilder = JwsHeader.with(jwsAlgorithm); + if (this.jwtCustomizer != null) { // @formatter:off - JwtEncodingContext.Builder jwtContextBuilder = JwtEncodingContext.with(headersBuilder, claimsBuilder) + JwtEncodingContext.Builder jwtContextBuilder = JwtEncodingContext.with(jwsHeaderBuilder, claimsBuilder) .registeredClient(context.getRegisteredClient()) .principal(context.getPrincipal()) .authorizationServerContext(context.getAuthorizationServerContext()) @@ -149,7 +153,7 @@ public final class JwtGenerator implements OAuth2TokenGenerator { this.jwtCustomizer.customize(jwtContext); } - JwsHeader jwsHeader = headersBuilder.build(); + JwsHeader jwsHeader = jwsHeaderBuilder.build(); JwtClaimsSet claims = claimsBuilder.build(); Jwt jwt = this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims)); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java index 8534ee60..83407b03 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java @@ -152,7 +152,10 @@ public class JwtGeneratorTests { @Test public void generateWhenIdTokenTypeThenReturnJwt() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build(); + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .scope(OidcScopes.OPENID) + .tokenSettings(TokenSettings.builder().idTokenSignatureAlgorithm(SignatureAlgorithm.ES256).build()) + .build(); Map authenticationRequestAdditionalParameters = new HashMap<>(); authenticationRequestAdditionalParameters.put(OidcParameterNames.NONCE, "nonce"); OAuth2Authorization authorization = TestOAuth2Authorizations.authorization( @@ -201,6 +204,13 @@ public class JwtGeneratorTests { ArgumentCaptor jwtEncoderParametersCaptor = ArgumentCaptor.forClass(JwtEncoderParameters.class); verify(this.jwtEncoder).encode(jwtEncoderParametersCaptor.capture()); + JwsHeader jwsHeader = jwtEncoderParametersCaptor.getValue().getJwsHeader(); + if (OidcParameterNames.ID_TOKEN.equals(tokenContext.getTokenType().getValue())) { + assertThat(jwsHeader.getAlgorithm()).isEqualTo(tokenContext.getRegisteredClient().getTokenSettings().getIdTokenSignatureAlgorithm()); + } else { + assertThat(jwsHeader.getAlgorithm()).isEqualTo(SignatureAlgorithm.RS256); + } + JwtClaimsSet jwtClaimsSet = jwtEncoderParametersCaptor.getValue().getClaims(); assertThat(jwtClaimsSet.getIssuer().toExternalForm()).isEqualTo(tokenContext.getAuthorizationServerContext().getIssuer()); assertThat(jwtClaimsSet.getSubject()).isEqualTo(tokenContext.getAuthorization().getPrincipalName()); @@ -208,20 +218,14 @@ public class JwtGeneratorTests { Instant issuedAt = Instant.now(); Instant expiresAt; - JwsHeader.Builder headersBuilder; if (tokenContext.getTokenType().equals(OAuth2TokenType.ACCESS_TOKEN)) { expiresAt = issuedAt.plus(tokenContext.getRegisteredClient().getTokenSettings().getAccessTokenTimeToLive()); - headersBuilder = JwsHeader.with(SignatureAlgorithm.RS256); } else { expiresAt = issuedAt.plus(30, ChronoUnit.MINUTES); - headersBuilder = JwsHeader.with(tokenContext.getRegisteredClient().getTokenSettings().getIdTokenSignatureAlgorithm()); } assertThat(jwtClaimsSet.getIssuedAt()).isBetween(issuedAt.minusSeconds(1), issuedAt.plusSeconds(1)); assertThat(jwtClaimsSet.getExpiresAt()).isBetween(expiresAt.minusSeconds(1), expiresAt.plusSeconds(1)); - JwsHeader jwsHeader = jwtEncoderParametersCaptor.getValue().getJwsHeader(); - assertThat(jwsHeader.getAlgorithm()).isEqualTo(headersBuilder.build().getAlgorithm()); - if (tokenContext.getTokenType().equals(OAuth2TokenType.ACCESS_TOKEN)) { assertThat(jwtClaimsSet.getNotBefore()).isBetween(issuedAt.minusSeconds(1), issuedAt.plusSeconds(1)); From 07d69cbfb4447f619f59171fa135afc9620e8e5e Mon Sep 17 00:00:00 2001 From: doctormacky Date: Thu, 18 Aug 2022 22:14:33 +0800 Subject: [PATCH 6/9] Validate client secret not expired Closes gh-850 --- .../ClientSecretAuthenticationProvider.java | 7 ++++++ ...ientSecretAuthenticationProviderTests.java | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/ClientSecretAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/ClientSecretAuthenticationProvider.java index 61eadb7b..974952dc 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/ClientSecretAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/ClientSecretAuthenticationProvider.java @@ -15,6 +15,8 @@ */ package org.springframework.security.oauth2.server.authorization.authentication; +import java.time.Instant; + import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -107,6 +109,11 @@ public final class ClientSecretAuthenticationProvider implements AuthenticationP throwInvalidClient(OAuth2ParameterNames.CLIENT_SECRET); } + if (registeredClient.getClientSecretExpiresAt() != null && + Instant.now().isAfter(registeredClient.getClientSecretExpiresAt())) { + throwInvalidClient("client_secret_expires_at"); + } + // Validate the "code_verifier" parameter for the confidential client, if available this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/ClientSecretAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/ClientSecretAuthenticationProviderTests.java index d20d2923..800beec0 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/ClientSecretAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/ClientSecretAuthenticationProviderTests.java @@ -15,6 +15,8 @@ */ package org.springframework.security.oauth2.server.authorization.authentication; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.Map; @@ -182,6 +184,26 @@ public class ClientSecretAuthenticationProviderTests { verify(this.passwordEncoder).matches(any(), any()); } + @Test + public void authenticateWhenExpiredClientSecretThenThrowOAuth2AuthenticationException() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .clientSecretExpiresAt(Instant.now().minus(1, ChronoUnit.HOURS).truncatedTo(ChronoUnit.SECONDS)) + .build(); + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( + registeredClient.getClientId(), ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret(), null); + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .satisfies(error -> { + assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); + assertThat(error.getDescription()).contains("client_secret_expires_at"); + }); + verify(this.passwordEncoder).matches(any(), any()); + } + @Test public void authenticateWhenValidCredentialsThenAuthenticated() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); From 2cc603c7e708290f9ffbd57c865d89e6caa87d6b Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Thu, 1 Sep 2022 17:02:14 -0400 Subject: [PATCH 7/9] Improve configurability for AuthenticationConverter and AuthenticationProvider Closes gh-417 --- .../docs/asciidoc/configuration-model.adoc | 18 ++-- .../src/docs/asciidoc/protocol-endpoints.adoc | 82 +++++++++++------- ...OAuth2AuthorizationEndpointConfigurer.java | 73 +++++++++++++--- .../OAuth2ClientAuthenticationConfigurer.java | 79 ++++++++++++++--- .../OAuth2TokenEndpointConfigurer.java | 80 ++++++++++++++--- ...2TokenIntrospectionEndpointConfigurer.java | 77 ++++++++++++++--- ...uth2TokenRevocationEndpointConfigurer.java | 79 ++++++++++++++--- ...Auth2TokenIntrospectionEndpointFilter.java | 54 +----------- .../OAuth2TokenRevocationEndpointFilter.java | 45 ++-------- ...nIntrospectionAuthenticationConverter.java | 86 +++++++++++++++++++ ...okenRevocationAuthenticationConverter.java | 74 ++++++++++++++++ .../OAuth2AuthorizationCodeGrantTests.java | 25 ++++++ .../OAuth2ClientCredentialsGrantTests.java | 78 ++++++++++++++++- .../OAuth2TokenIntrospectionTests.java | 27 ++++++ .../OAuth2TokenRevocationTests.java | 29 +++++++ 15 files changed, 709 insertions(+), 197 deletions(-) create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2TokenIntrospectionAuthenticationConverter.java create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2TokenRevocationAuthenticationConverter.java diff --git a/docs/src/docs/asciidoc/configuration-model.adoc b/docs/src/docs/asciidoc/configuration-model.adoc index 5614a158..32914f9e 100644 --- a/docs/src/docs/asciidoc/configuration-model.adoc +++ b/docs/src/docs/asciidoc/configuration-model.adoc @@ -202,18 +202,22 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h .clientAuthentication(clientAuthentication -> clientAuthentication .authenticationConverter(authenticationConverter) <1> - .authenticationProvider(authenticationProvider) <2> - .authenticationSuccessHandler(authenticationSuccessHandler) <3> - .errorResponseHandler(errorResponseHandler) <4> + .authenticationConverters(authenticationConvertersConsumer) <2> + .authenticationProvider(authenticationProvider) <3> + .authenticationProviders(authenticationProvidersConsumer) <4> + .authenticationSuccessHandler(authenticationSuccessHandler) <5> + .errorResponseHandler(errorResponseHandler) <6> ); return http.build(); } ---- -<1> `authenticationConverter()`: The `AuthenticationConverter` (_pre-processor_) used when attempting to extract client credentials from `HttpServletRequest` to an instance of `OAuth2ClientAuthenticationToken`. -<2> `authenticationProvider()`: The `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2ClientAuthenticationToken`. (One or more may be added to replace the defaults.) -<3> `authenticationSuccessHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling a successful client authentication and associating the `OAuth2ClientAuthenticationToken` to the `SecurityContext`. -<4> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling a failed client authentication and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-5.2[`OAuth2Error` response]. +<1> `authenticationConverter()`: Adds an `AuthenticationConverter` (_pre-processor_) used when attempting to extract client credentials from `HttpServletRequest` to an instance of `OAuth2ClientAuthenticationToken`. +<2> `authenticationConverters()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationConverter``'s allowing the ability to add, remove, or customize a specific `AuthenticationConverter`. +<3> `authenticationProvider()`: Adds an `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2ClientAuthenticationToken`. +<4> `authenticationProviders()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationProvider``'s allowing the ability to add, remove, or customize a specific `AuthenticationProvider`. +<5> `authenticationSuccessHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling a successful client authentication and associating the `OAuth2ClientAuthenticationToken` to the `SecurityContext`. +<6> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling a failed client authentication and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-5.2[`OAuth2Error` response]. `OAuth2ClientAuthenticationConfigurer` configures the `OAuth2ClientAuthenticationFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`. `OAuth2ClientAuthenticationFilter` is the `Filter` that processes client authentication requests. diff --git a/docs/src/docs/asciidoc/protocol-endpoints.adoc b/docs/src/docs/asciidoc/protocol-endpoints.adoc index 45b472e8..1a01c230 100644 --- a/docs/src/docs/asciidoc/protocol-endpoints.adoc +++ b/docs/src/docs/asciidoc/protocol-endpoints.adoc @@ -21,20 +21,24 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint .authorizationRequestConverter(authorizationRequestConverter) <1> - .authenticationProvider(authenticationProvider) <2> - .authorizationResponseHandler(authorizationResponseHandler) <3> - .errorResponseHandler(errorResponseHandler) <4> - .consentPage("/oauth2/v1/authorize") <5> + .authorizationRequestConverters(authorizationRequestConvertersConsumer) <2> + .authenticationProvider(authenticationProvider) <3> + .authenticationProviders(authenticationProvidersConsumer) <4> + .authorizationResponseHandler(authorizationResponseHandler) <5> + .errorResponseHandler(errorResponseHandler) <6> + .consentPage("/oauth2/v1/authorize") <7> ); return http.build(); } ---- -<1> `authorizationRequestConverter()`: The `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1[OAuth2 authorization request] (or consent) from `HttpServletRequest` to an instance of `OAuth2AuthorizationCodeRequestAuthenticationToken`. -<2> `authenticationProvider()`: The `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2AuthorizationCodeRequestAuthenticationToken`. (One or more may be added to replace the defaults.) -<3> `authorizationResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an "`authenticated`" `OAuth2AuthorizationCodeRequestAuthenticationToken` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2[OAuth2AuthorizationResponse]. -<4> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthorizationCodeRequestAuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1[OAuth2Error response]. -<5> `consentPage()`: The `URI` of the custom consent page to redirect resource owners to if consent is required during the authorization request flow. +<1> `authorizationRequestConverter()`: Adds an `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1[OAuth2 authorization request] (or consent) from `HttpServletRequest` to an instance of `OAuth2AuthorizationCodeRequestAuthenticationToken`. +<2> `authorizationRequestConverters()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationConverter``'s allowing the ability to add, remove, or customize a specific `AuthenticationConverter`. +<3> `authenticationProvider()`: Adds an `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2AuthorizationCodeRequestAuthenticationToken`. +<4> `authenticationProviders()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationProvider``'s allowing the ability to add, remove, or customize a specific `AuthenticationProvider`. +<5> `authorizationResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an "`authenticated`" `OAuth2AuthorizationCodeRequestAuthenticationToken` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2[OAuth2AuthorizationResponse]. +<6> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthorizationCodeRequestAuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1[OAuth2Error response]. +<7> `consentPage()`: The `URI` of the custom consent page to redirect resource owners to if consent is required during the authorization request flow. `OAuth2AuthorizationEndpointConfigurer` configures the `OAuth2AuthorizationEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`. `OAuth2AuthorizationEndpointFilter` is the `Filter` that processes OAuth2 authorization requests (and consents). @@ -65,18 +69,22 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h .tokenEndpoint(tokenEndpoint -> tokenEndpoint .accessTokenRequestConverter(accessTokenRequestConverter) <1> - .authenticationProvider(authenticationProvider) <2> - .accessTokenResponseHandler(accessTokenResponseHandler) <3> - .errorResponseHandler(errorResponseHandler) <4> + .accessTokenRequestConverters(accessTokenRequestConvertersConsumer) <2> + .authenticationProvider(authenticationProvider) <3> + .authenticationProviders(authenticationProvidersConsumer) <4> + .accessTokenResponseHandler(accessTokenResponseHandler) <5> + .errorResponseHandler(errorResponseHandler) <6> ); return http.build(); } ---- -<1> `accessTokenRequestConverter()`: The `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3[OAuth2 access token request] from `HttpServletRequest` to an instance of `OAuth2AuthorizationGrantAuthenticationToken`. -<2> `authenticationProvider()`: The `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2AuthorizationGrantAuthenticationToken`. (One or more may be added to replace the defaults.) -<3> `accessTokenResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an `OAuth2AccessTokenAuthenticationToken` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-5.1[`OAuth2AccessTokenResponse`]. -<4> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-5.2[OAuth2Error response]. +<1> `accessTokenRequestConverter()`: Adds an `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3[OAuth2 access token request] from `HttpServletRequest` to an instance of `OAuth2AuthorizationGrantAuthenticationToken`. +<2> `accessTokenRequestConverters()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationConverter``'s allowing the ability to add, remove, or customize a specific `AuthenticationConverter`. +<3> `authenticationProvider()`: Adds an `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2AuthorizationGrantAuthenticationToken`. +<4> `authenticationProviders()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationProvider``'s allowing the ability to add, remove, or customize a specific `AuthenticationProvider`. +<5> `accessTokenResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an `OAuth2AccessTokenAuthenticationToken` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-5.1[`OAuth2AccessTokenResponse`]. +<6> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-5.2[OAuth2Error response]. `OAuth2TokenEndpointConfigurer` configures the `OAuth2TokenEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`. `OAuth2TokenEndpointFilter` is the `Filter` that processes OAuth2 access token requests. @@ -110,25 +118,29 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h .tokenIntrospectionEndpoint(tokenIntrospectionEndpoint -> tokenIntrospectionEndpoint .introspectionRequestConverter(introspectionRequestConverter) <1> - .authenticationProvider(authenticationProvider) <2> - .introspectionResponseHandler(introspectionResponseHandler) <3> - .errorResponseHandler(errorResponseHandler) <4> + .introspectionRequestConverters(introspectionRequestConvertersConsumer) <2> + .authenticationProvider(authenticationProvider) <3> + .authenticationProviders(authenticationProvidersConsumer) <4> + .introspectionResponseHandler(introspectionResponseHandler) <5> + .errorResponseHandler(errorResponseHandler) <6> ); return http.build(); } ---- -<1> `introspectionRequestConverter()`: The `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc7662#section-2.1[OAuth2 introspection request] from `HttpServletRequest` to an instance of `OAuth2TokenIntrospectionAuthenticationToken`. -<2> `authenticationProvider()`: The `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2TokenIntrospectionAuthenticationToken`. (One or more may be added to replace the defaults.) -<3> `introspectionResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an "`authenticated`" `OAuth2TokenIntrospectionAuthenticationToken` and returning the https://datatracker.ietf.org/doc/html/rfc7662#section-2.2[OAuth2TokenIntrospection response]. -<4> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc7662#section-2.3[OAuth2Error response]. +<1> `introspectionRequestConverter()`: Adds an `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc7662#section-2.1[OAuth2 introspection request] from `HttpServletRequest` to an instance of `OAuth2TokenIntrospectionAuthenticationToken`. +<2> `introspectionRequestConverters()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationConverter``'s allowing the ability to add, remove, or customize a specific `AuthenticationConverter`. +<3> `authenticationProvider()`: Adds an `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2TokenIntrospectionAuthenticationToken`. +<4> `authenticationProviders()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationProvider``'s allowing the ability to add, remove, or customize a specific `AuthenticationProvider`. +<5> `introspectionResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an "`authenticated`" `OAuth2TokenIntrospectionAuthenticationToken` and returning the https://datatracker.ietf.org/doc/html/rfc7662#section-2.2[OAuth2TokenIntrospection response]. +<6> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc7662#section-2.3[OAuth2Error response]. `OAuth2TokenIntrospectionEndpointConfigurer` configures the `OAuth2TokenIntrospectionEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`. `OAuth2TokenIntrospectionEndpointFilter` is the `Filter` that processes OAuth2 introspection requests. `OAuth2TokenIntrospectionEndpointFilter` is configured with the following defaults: -* `*AuthenticationConverter*` -- An internal implementation that returns the `OAuth2TokenIntrospectionAuthenticationToken`. +* `*AuthenticationConverter*` -- An `OAuth2TokenIntrospectionAuthenticationConverter`. * `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OAuth2TokenIntrospectionAuthenticationProvider`. * `*AuthenticationSuccessHandler*` -- An internal implementation that handles an "`authenticated`" `OAuth2TokenIntrospectionAuthenticationToken` and returns the `OAuth2TokenIntrospection` response. * `*AuthenticationFailureHandler*` -- An internal implementation that uses the `OAuth2Error` associated with the `OAuth2AuthenticationException` and returns the `OAuth2Error` response. @@ -152,26 +164,30 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h authorizationServerConfigurer .tokenRevocationEndpoint(tokenRevocationEndpoint -> tokenRevocationEndpoint - .revocationRequestConverter(revocationRequestConverter) <1> - .authenticationProvider(authenticationProvider) <2> - .revocationResponseHandler(revocationResponseHandler) <3> - .errorResponseHandler(errorResponseHandler) <4> + .revocationRequestConverter(revocationRequestConverter) <1> + .revocationRequestConverters(revocationRequestConvertersConsumer) <2> + .authenticationProvider(authenticationProvider) <3> + .authenticationProviders(authenticationProvidersConsumer) <4> + .revocationResponseHandler(revocationResponseHandler) <5> + .errorResponseHandler(errorResponseHandler) <6> ); return http.build(); } ---- -<1> `revocationRequestConverter()`: The `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc7009#section-2.1[OAuth2 revocation request] from `HttpServletRequest` to an instance of `OAuth2TokenRevocationAuthenticationToken`. -<2> `authenticationProvider()`: The `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2TokenRevocationAuthenticationToken`. (One or more may be added to replace the defaults.) -<3> `revocationResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an "`authenticated`" `OAuth2TokenRevocationAuthenticationToken` and returning the https://datatracker.ietf.org/doc/html/rfc7009#section-2.2[OAuth2 revocation response]. -<4> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc7009#section-2.2.1[OAuth2Error response]. +<1> `revocationRequestConverter()`: Adds an `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc7009#section-2.1[OAuth2 revocation request] from `HttpServletRequest` to an instance of `OAuth2TokenRevocationAuthenticationToken`. +<2> `revocationRequestConverters()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationConverter``'s allowing the ability to add, remove, or customize a specific `AuthenticationConverter`. +<3> `authenticationProvider()`: Adds an `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2TokenRevocationAuthenticationToken`. +<4> `authenticationProviders()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationProvider``'s allowing the ability to add, remove, or customize a specific `AuthenticationProvider`. +<5> `revocationResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an "`authenticated`" `OAuth2TokenRevocationAuthenticationToken` and returning the https://datatracker.ietf.org/doc/html/rfc7009#section-2.2[OAuth2 revocation response]. +<6> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc7009#section-2.2.1[OAuth2Error response]. `OAuth2TokenRevocationEndpointConfigurer` configures the `OAuth2TokenRevocationEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`. `OAuth2TokenRevocationEndpointFilter` is the `Filter` that processes OAuth2 revocation requests. `OAuth2TokenRevocationEndpointFilter` is configured with the following defaults: -* `*AuthenticationConverter*` -- An internal implementation that returns the `OAuth2TokenRevocationAuthenticationToken`. +* `*AuthenticationConverter*` -- An `OAuth2TokenRevocationAuthenticationConverter`. * `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OAuth2TokenRevocationAuthenticationProvider`. * `*AuthenticationSuccessHandler*` -- An internal implementation that handles an "`authenticated`" `OAuth2TokenRevocationAuthenticationToken` and returns the OAuth2 revocation response. * `*AuthenticationFailureHandler*` -- An internal implementation that uses the `OAuth2Error` associated with the `OAuth2AuthenticationException` and returns the `OAuth2Error` response. diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationEndpointConfigurer.java index c946087b..99ec36f7 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationEndpointConfigurer.java @@ -17,6 +17,7 @@ package org.springframework.security.oauth2.server.authorization.config.annotati import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import javax.servlet.http.HttpServletRequest; @@ -32,6 +33,8 @@ import org.springframework.security.oauth2.server.authorization.authentication.O import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken; import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter; +import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; @@ -52,8 +55,10 @@ import org.springframework.util.StringUtils; */ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2Configurer { private RequestMatcher requestMatcher; - private AuthenticationConverter authorizationRequestConverter; + private final List authorizationRequestConverters = new ArrayList<>(); + private Consumer> authorizationRequestConvertersConsumer = (authorizationRequestConverters) -> {}; private final List authenticationProviders = new ArrayList<>(); + private Consumer> authenticationProvidersConsumer = (authenticationProviders) -> {}; private AuthenticationSuccessHandler authorizationResponseHandler; private AuthenticationFailureHandler errorResponseHandler; private String consentPage; @@ -66,14 +71,31 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C } /** - * Sets the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest} + * Adds an {@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} + * @param authorizationRequestConverter an {@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; + Assert.notNull(authorizationRequestConverter, "authorizationRequestConverter cannot be null"); + this.authorizationRequestConverters.add(authorizationRequestConverter); + return this; + } + + /** + * Sets the {@code Consumer} providing access to the {@code List} of default + * and (optionally) added {@link #authorizationRequestConverter(AuthenticationConverter) AuthenticationConverter}'s + * allowing the ability to add, remove, or customize a specific {@link AuthenticationConverter}. + * + * @param authorizationRequestConvertersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationConverter}'s + * @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration + * @since 0.4.0 + */ + public OAuth2AuthorizationEndpointConfigurer authorizationRequestConverters( + Consumer> authorizationRequestConvertersConsumer) { + Assert.notNull(authorizationRequestConvertersConsumer, "authorizationRequestConvertersConsumer cannot be null"); + this.authorizationRequestConvertersConsumer = authorizationRequestConvertersConsumer; return this; } @@ -89,6 +111,22 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C return this; } + /** + * Sets the {@code Consumer} providing access to the {@code List} of default + * and (optionally) added {@link #authenticationProvider(AuthenticationProvider) AuthenticationProvider}'s + * allowing the ability to add, remove, or customize a specific {@link AuthenticationProvider}. + * + * @param authenticationProvidersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationProvider}'s + * @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration + * @since 0.4.0 + */ + public OAuth2AuthorizationEndpointConfigurer authenticationProviders( + Consumer> authenticationProvidersConsumer) { + Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null"); + this.authenticationProvidersConsumer = authenticationProvidersConsumer; + return this; + } + /** * Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationToken} * and returning the {@link OAuth2AuthorizationResponse Authorization Response}. @@ -158,10 +196,11 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C authorizationServerSettings.getAuthorizationEndpoint(), HttpMethod.POST.name())); - List authenticationProviders = - !this.authenticationProviders.isEmpty() ? - this.authenticationProviders : - createDefaultAuthenticationProviders(httpSecurity); + List authenticationProviders = createDefaultAuthenticationProviders(httpSecurity); + if (!this.authenticationProviders.isEmpty()) { + authenticationProviders.addAll(0, this.authenticationProviders); + } + this.authenticationProvidersConsumer.accept(authenticationProviders); authenticationProviders.forEach(authenticationProvider -> httpSecurity.authenticationProvider(postProcess(authenticationProvider))); } @@ -175,9 +214,13 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C new OAuth2AuthorizationEndpointFilter( authenticationManager, authorizationServerSettings.getAuthorizationEndpoint()); - if (this.authorizationRequestConverter != null) { - authorizationEndpointFilter.setAuthenticationConverter(this.authorizationRequestConverter); + List authenticationConverters = createDefaultAuthenticationConverters(); + if (!this.authorizationRequestConverters.isEmpty()) { + authenticationConverters.addAll(0, this.authorizationRequestConverters); } + this.authorizationRequestConvertersConsumer.accept(authenticationConverters); + authorizationEndpointFilter.setAuthenticationConverter( + new DelegatingAuthenticationConverter(authenticationConverters)); if (this.authorizationResponseHandler != null) { authorizationEndpointFilter.setAuthenticationSuccessHandler(this.authorizationResponseHandler); } @@ -195,7 +238,15 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C return this.requestMatcher; } - private List createDefaultAuthenticationProviders(HttpSecurity httpSecurity) { + private static List createDefaultAuthenticationConverters() { + List authenticationConverters = new ArrayList<>(); + + authenticationConverters.add(new OAuth2AuthorizationCodeRequestAuthenticationConverter()); + + return authenticationConverters; + } + + private static List createDefaultAuthenticationProviders(HttpSecurity httpSecurity) { List authenticationProviders = new ArrayList<>(); OAuth2AuthorizationCodeRequestAuthenticationProvider authorizationCodeRequestAuthenticationProvider = diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java index 7fb5b077..eecb1b87 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java @@ -17,6 +17,7 @@ package org.springframework.security.oauth2.server.authorization.config.annotati import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import javax.servlet.http.HttpServletRequest; @@ -36,6 +37,11 @@ import org.springframework.security.oauth2.server.authorization.authentication.P import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter; +import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretBasicAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretPostAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.JwtClientAssertionAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.PublicClientAuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; @@ -55,8 +61,10 @@ import org.springframework.util.Assert; */ public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Configurer { private RequestMatcher requestMatcher; - private AuthenticationConverter authenticationConverter; + private final List authenticationConverters = new ArrayList<>(); + private Consumer> authenticationConvertersConsumer = (authenticationConverters) -> {}; private final List authenticationProviders = new ArrayList<>(); + private Consumer> authenticationProvidersConsumer = (authenticationProviders) -> {}; private AuthenticationSuccessHandler authenticationSuccessHandler; private AuthenticationFailureHandler errorResponseHandler; @@ -68,14 +76,31 @@ public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Co } /** - * Sets the {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest} + * Adds an {@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} + * @param authenticationConverter an {@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; + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + this.authenticationConverters.add(authenticationConverter); + return this; + } + + /** + * Sets the {@code Consumer} providing access to the {@code List} of default + * and (optionally) added {@link #authenticationConverter(AuthenticationConverter) AuthenticationConverter}'s + * allowing the ability to add, remove, or customize a specific {@link AuthenticationConverter}. + * + * @param authenticationConvertersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationConverter}'s + * @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration + * @since 0.4.0 + */ + public OAuth2ClientAuthenticationConfigurer authenticationConverters( + Consumer> authenticationConvertersConsumer) { + Assert.notNull(authenticationConvertersConsumer, "authenticationConvertersConsumer cannot be null"); + this.authenticationConvertersConsumer = authenticationConvertersConsumer; return this; } @@ -91,6 +116,22 @@ public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Co return this; } + /** + * Sets the {@code Consumer} providing access to the {@code List} of default + * and (optionally) added {@link #authenticationProvider(AuthenticationProvider) AuthenticationProvider}'s + * allowing the ability to add, remove, or customize a specific {@link AuthenticationProvider}. + * + * @param authenticationProvidersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationProvider}'s + * @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration + * @since 0.4.0 + */ + public OAuth2ClientAuthenticationConfigurer authenticationProviders( + Consumer> authenticationProvidersConsumer) { + Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null"); + this.authenticationProvidersConsumer = authenticationProvidersConsumer; + return this; + } + /** * Sets the {@link AuthenticationSuccessHandler} used for handling a successful client authentication * and associating the {@link OAuth2ClientAuthenticationToken} to the {@link SecurityContext}. @@ -129,10 +170,11 @@ public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Co authorizationServerSettings.getTokenRevocationEndpoint(), HttpMethod.POST.name())); - List authenticationProviders = - !this.authenticationProviders.isEmpty() ? - this.authenticationProviders : - createDefaultAuthenticationProviders(httpSecurity); + List authenticationProviders = createDefaultAuthenticationProviders(httpSecurity); + if (!this.authenticationProviders.isEmpty()) { + authenticationProviders.addAll(0, this.authenticationProviders); + } + this.authenticationProvidersConsumer.accept(authenticationProviders); authenticationProviders.forEach(authenticationProvider -> httpSecurity.authenticationProvider(postProcess(authenticationProvider))); } @@ -142,9 +184,13 @@ public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Co AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class); OAuth2ClientAuthenticationFilter clientAuthenticationFilter = new OAuth2ClientAuthenticationFilter( authenticationManager, this.requestMatcher); - if (this.authenticationConverter != null) { - clientAuthenticationFilter.setAuthenticationConverter(this.authenticationConverter); + List authenticationConverters = createDefaultAuthenticationConverters(); + if (!this.authenticationConverters.isEmpty()) { + authenticationConverters.addAll(0, this.authenticationConverters); } + this.authenticationConvertersConsumer.accept(authenticationConverters); + clientAuthenticationFilter.setAuthenticationConverter( + new DelegatingAuthenticationConverter(authenticationConverters)); if (this.authenticationSuccessHandler != null) { clientAuthenticationFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler); } @@ -159,7 +205,18 @@ public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Co return this.requestMatcher; } - private List createDefaultAuthenticationProviders(HttpSecurity httpSecurity) { + private static List createDefaultAuthenticationConverters() { + List authenticationConverters = new ArrayList<>(); + + authenticationConverters.add(new JwtClientAssertionAuthenticationConverter()); + authenticationConverters.add(new ClientSecretBasicAuthenticationConverter()); + authenticationConverters.add(new ClientSecretPostAuthenticationConverter()); + authenticationConverters.add(new PublicClientAuthenticationConverter()); + + return authenticationConverters; + } + + private static List createDefaultAuthenticationProviders(HttpSecurity httpSecurity) { List authenticationProviders = new ArrayList<>(); RegisteredClientRepository registeredClientRepository = OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java index e54f6652..5a8fe94f 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java @@ -16,8 +16,8 @@ package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; +import java.util.function.Consumer; import javax.servlet.http.HttpServletRequest; @@ -39,6 +39,10 @@ import org.springframework.security.oauth2.server.authorization.authentication.O import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter; +import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; @@ -57,8 +61,10 @@ import org.springframework.util.Assert; */ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configurer { private RequestMatcher requestMatcher; - private AuthenticationConverter accessTokenRequestConverter; - private final List authenticationProviders = new LinkedList<>(); + private final List accessTokenRequestConverters = new ArrayList<>(); + private Consumer> accessTokenRequestConvertersConsumer = (accessTokenRequestConverters) -> {}; + private final List authenticationProviders = new ArrayList<>(); + private Consumer> authenticationProvidersConsumer = (authenticationProviders) -> {}; private AuthenticationSuccessHandler accessTokenResponseHandler; private AuthenticationFailureHandler errorResponseHandler; @@ -70,14 +76,31 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure } /** - * Sets the {@link AuthenticationConverter} used when attempting to extract an Access Token Request from {@link HttpServletRequest} + * Adds an {@link AuthenticationConverter} used when attempting to extract an Access Token Request from {@link HttpServletRequest} * to an instance of {@link OAuth2AuthorizationGrantAuthenticationToken} used for authenticating the authorization grant. * - * @param accessTokenRequestConverter the {@link AuthenticationConverter} used when attempting to extract an Access Token Request from {@link HttpServletRequest} + * @param accessTokenRequestConverter an {@link AuthenticationConverter} used when attempting to extract an Access Token Request from {@link HttpServletRequest} * @return the {@link OAuth2TokenEndpointConfigurer} for further configuration */ public OAuth2TokenEndpointConfigurer accessTokenRequestConverter(AuthenticationConverter accessTokenRequestConverter) { - this.accessTokenRequestConverter = accessTokenRequestConverter; + Assert.notNull(accessTokenRequestConverter, "accessTokenRequestConverter cannot be null"); + this.accessTokenRequestConverters.add(accessTokenRequestConverter); + return this; + } + + /** + * Sets the {@code Consumer} providing access to the {@code List} of default + * and (optionally) added {@link #accessTokenRequestConverter(AuthenticationConverter) AuthenticationConverter}'s + * allowing the ability to add, remove, or customize a specific {@link AuthenticationConverter}. + * + * @param accessTokenRequestConvertersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationConverter}'s + * @return the {@link OAuth2TokenEndpointConfigurer} for further configuration + * @since 0.4.0 + */ + public OAuth2TokenEndpointConfigurer accessTokenRequestConverters( + Consumer> accessTokenRequestConvertersConsumer) { + Assert.notNull(accessTokenRequestConvertersConsumer, "accessTokenRequestConvertersConsumer cannot be null"); + this.accessTokenRequestConvertersConsumer = accessTokenRequestConvertersConsumer; return this; } @@ -93,6 +116,22 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure return this; } + /** + * Sets the {@code Consumer} providing access to the {@code List} of default + * and (optionally) added {@link #authenticationProvider(AuthenticationProvider) AuthenticationProvider}'s + * allowing the ability to add, remove, or customize a specific {@link AuthenticationProvider}. + * + * @param authenticationProvidersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationProvider}'s + * @return the {@link OAuth2TokenEndpointConfigurer} for further configuration + * @since 0.4.0 + */ + public OAuth2TokenEndpointConfigurer authenticationProviders( + Consumer> authenticationProvidersConsumer) { + Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null"); + this.authenticationProvidersConsumer = authenticationProvidersConsumer; + return this; + } + /** * Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AccessTokenAuthenticationToken} * and returning the {@link OAuth2AccessTokenResponse Access Token Response}. @@ -123,10 +162,11 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure this.requestMatcher = new AntPathRequestMatcher( authorizationServerSettings.getTokenEndpoint(), HttpMethod.POST.name()); - List authenticationProviders = - !this.authenticationProviders.isEmpty() ? - this.authenticationProviders : - createDefaultAuthenticationProviders(httpSecurity); + List authenticationProviders = createDefaultAuthenticationProviders(httpSecurity); + if (!this.authenticationProviders.isEmpty()) { + authenticationProviders.addAll(0, this.authenticationProviders); + } + this.authenticationProvidersConsumer.accept(authenticationProviders); authenticationProviders.forEach(authenticationProvider -> httpSecurity.authenticationProvider(postProcess(authenticationProvider))); } @@ -140,9 +180,13 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure new OAuth2TokenEndpointFilter( authenticationManager, authorizationServerSettings.getTokenEndpoint()); - if (this.accessTokenRequestConverter != null) { - tokenEndpointFilter.setAuthenticationConverter(this.accessTokenRequestConverter); + List authenticationConverters = createDefaultAuthenticationConverters(); + if (!this.accessTokenRequestConverters.isEmpty()) { + authenticationConverters.addAll(0, this.accessTokenRequestConverters); } + this.accessTokenRequestConvertersConsumer.accept(authenticationConverters); + tokenEndpointFilter.setAuthenticationConverter( + new DelegatingAuthenticationConverter(authenticationConverters)); if (this.accessTokenResponseHandler != null) { tokenEndpointFilter.setAuthenticationSuccessHandler(this.accessTokenResponseHandler); } @@ -157,7 +201,17 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure return this.requestMatcher; } - private List createDefaultAuthenticationProviders(HttpSecurity httpSecurity) { + private static List createDefaultAuthenticationConverters() { + List authenticationConverters = new ArrayList<>(); + + authenticationConverters.add(new OAuth2AuthorizationCodeAuthenticationConverter()); + authenticationConverters.add(new OAuth2RefreshTokenAuthenticationConverter()); + authenticationConverters.add(new OAuth2ClientCredentialsAuthenticationConverter()); + + return authenticationConverters; + } + + private static List createDefaultAuthenticationProviders(HttpSecurity httpSecurity) { List authenticationProviders = new ArrayList<>(); OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionEndpointConfigurer.java index d6940425..f9c687dc 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionEndpointConfigurer.java @@ -16,8 +16,8 @@ package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; +import java.util.function.Consumer; import javax.servlet.http.HttpServletRequest; @@ -33,6 +33,8 @@ import org.springframework.security.oauth2.server.authorization.authentication.O import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken; import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter; +import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenIntrospectionAuthenticationConverter; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; @@ -45,14 +47,17 @@ import org.springframework.util.Assert; * Configurer for the OAuth 2.0 Token Introspection Endpoint. * * @author Gaurav Tiwari + * @author Joe Grandja * @since 0.2.3 * @see OAuth2AuthorizationServerConfigurer#tokenIntrospectionEndpoint(Customizer) * @see OAuth2TokenIntrospectionEndpointFilter */ public final class OAuth2TokenIntrospectionEndpointConfigurer extends AbstractOAuth2Configurer { private RequestMatcher requestMatcher; - private AuthenticationConverter introspectionRequestConverter; - private final List authenticationProviders = new LinkedList<>(); + private final List introspectionRequestConverters = new ArrayList<>(); + private Consumer> introspectionRequestConvertersConsumer = (introspectionRequestConverters) -> {}; + private final List authenticationProviders = new ArrayList<>(); + private Consumer> authenticationProvidersConsumer = (authenticationProviders) -> {}; private AuthenticationSuccessHandler introspectionResponseHandler; private AuthenticationFailureHandler errorResponseHandler; @@ -64,14 +69,31 @@ public final class OAuth2TokenIntrospectionEndpointConfigurer extends AbstractOA } /** - * Sets the {@link AuthenticationConverter} used when attempting to extract an Introspection Request from {@link HttpServletRequest} + * Adds an {@link AuthenticationConverter} used when attempting to extract an Introspection Request from {@link HttpServletRequest} * to an instance of {@link OAuth2TokenIntrospectionAuthenticationToken} used for authenticating the request. * - * @param introspectionRequestConverter the {@link AuthenticationConverter} used when attempting to extract an Introspection Request from {@link HttpServletRequest} + * @param introspectionRequestConverter an {@link AuthenticationConverter} used when attempting to extract an Introspection Request from {@link HttpServletRequest} * @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further configuration */ public OAuth2TokenIntrospectionEndpointConfigurer introspectionRequestConverter(AuthenticationConverter introspectionRequestConverter) { - this.introspectionRequestConverter = introspectionRequestConverter; + Assert.notNull(introspectionRequestConverter, "introspectionRequestConverter cannot be null"); + this.introspectionRequestConverters.add(introspectionRequestConverter); + return this; + } + + /** + * Sets the {@code Consumer} providing access to the {@code List} of default + * and (optionally) added {@link #introspectionRequestConverter(AuthenticationConverter) AuthenticationConverter}'s + * allowing the ability to add, remove, or customize a specific {@link AuthenticationConverter}. + * + * @param introspectionRequestConvertersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationConverter}'s + * @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further configuration + * @since 0.4.0 + */ + public OAuth2TokenIntrospectionEndpointConfigurer introspectionRequestConverters( + Consumer> introspectionRequestConvertersConsumer) { + Assert.notNull(introspectionRequestConvertersConsumer, "introspectionRequestConvertersConsumer cannot be null"); + this.introspectionRequestConvertersConsumer = introspectionRequestConvertersConsumer; return this; } @@ -87,6 +109,22 @@ public final class OAuth2TokenIntrospectionEndpointConfigurer extends AbstractOA return this; } + /** + * Sets the {@code Consumer} providing access to the {@code List} of default + * and (optionally) added {@link #authenticationProvider(AuthenticationProvider) AuthenticationProvider}'s + * allowing the ability to add, remove, or customize a specific {@link AuthenticationProvider}. + * + * @param authenticationProvidersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationProvider}'s + * @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further configuration + * @since 0.4.0 + */ + public OAuth2TokenIntrospectionEndpointConfigurer authenticationProviders( + Consumer> authenticationProvidersConsumer) { + Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null"); + this.authenticationProvidersConsumer = authenticationProvidersConsumer; + return this; + } + /** * Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenIntrospectionAuthenticationToken}. * @@ -116,10 +154,11 @@ public final class OAuth2TokenIntrospectionEndpointConfigurer extends AbstractOA this.requestMatcher = new AntPathRequestMatcher( authorizationServerSettings.getTokenIntrospectionEndpoint(), HttpMethod.POST.name()); - List authenticationProviders = - !this.authenticationProviders.isEmpty() ? - this.authenticationProviders : - createDefaultAuthenticationProviders(httpSecurity); + List authenticationProviders = createDefaultAuthenticationProviders(httpSecurity); + if (!this.authenticationProviders.isEmpty()) { + authenticationProviders.addAll(0, this.authenticationProviders); + } + this.authenticationProvidersConsumer.accept(authenticationProviders); authenticationProviders.forEach(authenticationProvider -> httpSecurity.authenticationProvider(postProcess(authenticationProvider))); } @@ -132,9 +171,13 @@ public final class OAuth2TokenIntrospectionEndpointConfigurer extends AbstractOA OAuth2TokenIntrospectionEndpointFilter introspectionEndpointFilter = new OAuth2TokenIntrospectionEndpointFilter( authenticationManager, authorizationServerSettings.getTokenIntrospectionEndpoint()); - if (this.introspectionRequestConverter != null) { - introspectionEndpointFilter.setAuthenticationConverter(this.introspectionRequestConverter); + List authenticationConverters = createDefaultAuthenticationConverters(); + if (!this.introspectionRequestConverters.isEmpty()) { + authenticationConverters.addAll(0, this.introspectionRequestConverters); } + this.introspectionRequestConvertersConsumer.accept(authenticationConverters); + introspectionEndpointFilter.setAuthenticationConverter( + new DelegatingAuthenticationConverter(authenticationConverters)); if (this.introspectionResponseHandler != null) { introspectionEndpointFilter.setAuthenticationSuccessHandler(this.introspectionResponseHandler); } @@ -149,7 +192,15 @@ public final class OAuth2TokenIntrospectionEndpointConfigurer extends AbstractOA return this.requestMatcher; } - private List createDefaultAuthenticationProviders(HttpSecurity httpSecurity) { + private static List createDefaultAuthenticationConverters() { + List authenticationConverters = new ArrayList<>(); + + authenticationConverters.add(new OAuth2TokenIntrospectionAuthenticationConverter()); + + return authenticationConverters; + } + + private static List createDefaultAuthenticationProviders(HttpSecurity httpSecurity) { List authenticationProviders = new ArrayList<>(); OAuth2TokenIntrospectionAuthenticationProvider tokenIntrospectionAuthenticationProvider = diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationEndpointConfigurer.java index 6e26a821..0b567164 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationEndpointConfigurer.java @@ -16,8 +16,8 @@ package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; +import java.util.function.Consumer; import javax.servlet.http.HttpServletRequest; @@ -32,6 +32,8 @@ 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.settings.AuthorizationServerSettings; import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter; +import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenRevocationAuthenticationConverter; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; @@ -44,14 +46,17 @@ import org.springframework.util.Assert; * Configurer for the OAuth 2.0 Token Revocation Endpoint. * * @author Arfat Chaus + * @author Joe Grandja * @since 0.2.2 * @see OAuth2AuthorizationServerConfigurer#tokenRevocationEndpoint * @see OAuth2TokenRevocationEndpointFilter */ public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth2Configurer { private RequestMatcher requestMatcher; - private AuthenticationConverter revocationRequestConverter; - private final List authenticationProviders = new LinkedList<>(); + private final List revocationRequestConverters = new ArrayList<>(); + private Consumer> revocationRequestConvertersConsumer = (revocationRequestConverters) -> {}; + private final List authenticationProviders = new ArrayList<>(); + private Consumer> authenticationProvidersConsumer = (authenticationProviders) -> {}; private AuthenticationSuccessHandler revocationResponseHandler; private AuthenticationFailureHandler errorResponseHandler; @@ -63,14 +68,31 @@ public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth } /** - * 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. + * Adds an {@link AuthenticationConverter} used when attempting to extract a Revoke Token Request from {@link HttpServletRequest} + * to an instance of {@link OAuth2TokenRevocationAuthenticationToken} used for authenticating the request. * - * @param revocationRequestConverter the {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest} + * @param revocationRequestConverter an {@link AuthenticationConverter} used when attempting to extract a Revoke Token Request from {@link HttpServletRequest} * @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further configuration */ public OAuth2TokenRevocationEndpointConfigurer revocationRequestConverter(AuthenticationConverter revocationRequestConverter) { - this.revocationRequestConverter = revocationRequestConverter; + Assert.notNull(revocationRequestConverter, "revocationRequestConverter cannot be null"); + this.revocationRequestConverters.add(revocationRequestConverter); + return this; + } + + /** + * Sets the {@code Consumer} providing access to the {@code List} of default + * and (optionally) added {@link #revocationRequestConverter(AuthenticationConverter) AuthenticationConverter}'s + * allowing the ability to add, remove, or customize a specific {@link AuthenticationConverter}. + * + * @param revocationRequestConvertersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationConverter}'s + * @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further configuration + * @since 0.4.0 + */ + public OAuth2TokenRevocationEndpointConfigurer revocationRequestConverters( + Consumer> revocationRequestConvertersConsumer) { + Assert.notNull(revocationRequestConvertersConsumer, "revocationRequestConvertersConsumer cannot be null"); + this.revocationRequestConvertersConsumer = revocationRequestConvertersConsumer; return this; } @@ -86,6 +108,22 @@ public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth return this; } + /** + * Sets the {@code Consumer} providing access to the {@code List} of default + * and (optionally) added {@link #authenticationProvider(AuthenticationProvider) AuthenticationProvider}'s + * allowing the ability to add, remove, or customize a specific {@link AuthenticationProvider}. + * + * @param authenticationProvidersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationProvider}'s + * @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further configuration + * @since 0.4.0 + */ + public OAuth2TokenRevocationEndpointConfigurer authenticationProviders( + Consumer> authenticationProvidersConsumer) { + Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null"); + this.authenticationProvidersConsumer = authenticationProvidersConsumer; + return this; + } + /** * Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenRevocationAuthenticationToken}. * @@ -115,10 +153,11 @@ public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth this.requestMatcher = new AntPathRequestMatcher( authorizationServerSettings.getTokenRevocationEndpoint(), HttpMethod.POST.name()); - List authenticationProviders = - !this.authenticationProviders.isEmpty() ? - this.authenticationProviders : - createDefaultAuthenticationProviders(httpSecurity); + List authenticationProviders = createDefaultAuthenticationProviders(httpSecurity); + if (!this.authenticationProviders.isEmpty()) { + authenticationProviders.addAll(0, this.authenticationProviders); + } + this.authenticationProvidersConsumer.accept(authenticationProviders); authenticationProviders.forEach(authenticationProvider -> httpSecurity.authenticationProvider(postProcess(authenticationProvider))); } @@ -131,9 +170,13 @@ public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth OAuth2TokenRevocationEndpointFilter revocationEndpointFilter = new OAuth2TokenRevocationEndpointFilter( authenticationManager, authorizationServerSettings.getTokenRevocationEndpoint()); - if (this.revocationRequestConverter != null) { - revocationEndpointFilter.setAuthenticationConverter(this.revocationRequestConverter); + List authenticationConverters = createDefaultAuthenticationConverters(); + if (!this.revocationRequestConverters.isEmpty()) { + authenticationConverters.addAll(0, this.revocationRequestConverters); } + this.revocationRequestConvertersConsumer.accept(authenticationConverters); + revocationEndpointFilter.setAuthenticationConverter( + new DelegatingAuthenticationConverter(authenticationConverters)); if (this.revocationResponseHandler != null) { revocationEndpointFilter.setAuthenticationSuccessHandler(this.revocationResponseHandler); } @@ -148,7 +191,15 @@ public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth return this.requestMatcher; } - private List createDefaultAuthenticationProviders(HttpSecurity httpSecurity) { + private static List createDefaultAuthenticationConverters() { + List authenticationConverters = new ArrayList<>(); + + authenticationConverters.add(new OAuth2TokenRevocationAuthenticationConverter()); + + return authenticationConverters; + } + + private static List createDefaultAuthenticationProviders(HttpSecurity httpSecurity) { List authenticationProviders = new ArrayList<>(); OAuth2TokenRevocationAuthenticationProvider tokenRevocationAuthenticationProvider = diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilter.java index 14c0c5d0..705f1ac8 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilter.java @@ -16,8 +16,6 @@ package org.springframework.security.oauth2.server.authorization.web; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -34,21 +32,18 @@ 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; -import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter; import org.springframework.security.oauth2.server.authorization.OAuth2TokenIntrospection; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken; import org.springframework.security.oauth2.server.authorization.http.converter.OAuth2TokenIntrospectionHttpMessageConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenIntrospectionAuthenticationConverter; 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; -import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; /** @@ -70,8 +65,7 @@ public final class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequest private final AuthenticationManager authenticationManager; private final RequestMatcher tokenIntrospectionEndpointMatcher; - private AuthenticationConverter authenticationConverter = - new DefaultTokenIntrospectionAuthenticationConverter(); + private AuthenticationConverter authenticationConverter; private final HttpMessageConverter tokenIntrospectionHttpResponseConverter = new OAuth2TokenIntrospectionHttpMessageConverter(); private final HttpMessageConverter errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter(); @@ -100,6 +94,7 @@ public final class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequest this.authenticationManager = authenticationManager; this.tokenIntrospectionEndpointMatcher = new AntPathRequestMatcher( tokenIntrospectionEndpointUri, HttpMethod.POST.name()); + this.authenticationConverter = new OAuth2TokenIntrospectionAuthenticationConverter(); } @Override @@ -175,47 +170,4 @@ public final class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequest this.errorHttpResponseConverter.write(error, null, httpResponse); } - private static void throwError(String errorCode, String parameterName) { - OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Token Introspection Parameter: " + parameterName, - "https://datatracker.ietf.org/doc/html/rfc7662#section-2.1"); - throw new OAuth2AuthenticationException(error); - } - - private static class DefaultTokenIntrospectionAuthenticationConverter - implements AuthenticationConverter { - - @Override - public Authentication convert(HttpServletRequest request) { - Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); - - MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); - - // token (REQUIRED) - String token = parameters.getFirst(OAuth2ParameterNames.TOKEN); - if (!StringUtils.hasText(token) || - parameters.get(OAuth2ParameterNames.TOKEN).size() != 1) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN); - } - - // token_type_hint (OPTIONAL) - String tokenTypeHint = parameters.getFirst(OAuth2ParameterNames.TOKEN_TYPE_HINT); - if (StringUtils.hasText(tokenTypeHint) && - parameters.get(OAuth2ParameterNames.TOKEN_TYPE_HINT).size() != 1) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN_TYPE_HINT); - } - - Map additionalParameters = new HashMap<>(); - parameters.forEach((key, value) -> { - if (!key.equals(OAuth2ParameterNames.TOKEN) && - !key.equals(OAuth2ParameterNames.TOKEN_TYPE_HINT)) { - additionalParameters.put(key, value.get(0)); - } - }); - - return new OAuth2TokenIntrospectionAuthenticationToken( - token, clientPrincipal, tokenTypeHint, additionalParameters); - } - - } - } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenRevocationEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenRevocationEndpointFilter.java index 7a9c4be9..dad33c4f 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenRevocationEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenRevocationEndpointFilter.java @@ -32,19 +32,16 @@ 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; -import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -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.oauth2.server.authorization.web.authentication.OAuth2TokenRevocationAuthenticationConverter; 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; -import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; /** @@ -66,8 +63,7 @@ public final class OAuth2TokenRevocationEndpointFilter extends OncePerRequestFil private final AuthenticationManager authenticationManager; private final RequestMatcher tokenRevocationEndpointMatcher; - private AuthenticationConverter authenticationConverter = - new DefaultTokenRevocationAuthenticationConverter(); + private AuthenticationConverter authenticationConverter; private final HttpMessageConverter errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter(); private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendRevocationSuccessResponse; @@ -95,6 +91,7 @@ public final class OAuth2TokenRevocationEndpointFilter extends OncePerRequestFil this.authenticationManager = authenticationManager; this.tokenRevocationEndpointMatcher = new AntPathRequestMatcher( tokenRevocationEndpointUri, HttpMethod.POST.name()); + this.authenticationConverter = new OAuth2TokenRevocationAuthenticationConverter(); } @Override @@ -119,9 +116,9 @@ public final class OAuth2TokenRevocationEndpointFilter extends OncePerRequestFil /** * 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. + * to an instance of {@link OAuth2TokenRevocationAuthenticationToken} used for authenticating the request. * - * @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest} + * @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract a Revoke Token Request from {@link HttpServletRequest} * @since 0.2.2 */ public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) { @@ -164,36 +161,4 @@ public final class OAuth2TokenRevocationEndpointFilter extends OncePerRequestFil this.errorHttpResponseConverter.write(error, null, httpResponse); } - private static void throwError(String errorCode, String parameterName) { - OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Token Revocation Parameter: " + parameterName, - "https://datatracker.ietf.org/doc/html/rfc7009#section-2.1"); - throw new OAuth2AuthenticationException(error); - } - - private static class DefaultTokenRevocationAuthenticationConverter - implements AuthenticationConverter { - - @Override - public Authentication convert(HttpServletRequest request) { - Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); - - MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); - - // token (REQUIRED) - String token = parameters.getFirst(OAuth2ParameterNames.TOKEN); - if (!StringUtils.hasText(token) || - parameters.get(OAuth2ParameterNames.TOKEN).size() != 1) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN); - } - - // token_type_hint (OPTIONAL) - String tokenTypeHint = parameters.getFirst(OAuth2ParameterNames.TOKEN_TYPE_HINT); - if (StringUtils.hasText(tokenTypeHint) && - parameters.get(OAuth2ParameterNames.TOKEN_TYPE_HINT).size() != 1) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN_TYPE_HINT); - } - - return new OAuth2TokenRevocationAuthenticationToken(token, clientPrincipal, tokenTypeHint); - } - } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2TokenIntrospectionAuthenticationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2TokenIntrospectionAuthenticationConverter.java new file mode 100644 index 00000000..94a3dbd5 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2TokenIntrospectionAuthenticationConverter.java @@ -0,0 +1,86 @@ +/* + * Copyright 2020-2022 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.oauth2.server.authorization.web.authentication; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +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.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +/** + * Attempts to extract an Introspection Request from {@link HttpServletRequest} + * and then converts it to an {@link OAuth2TokenIntrospectionAuthenticationToken} used for authenticating the request. + * + * @author Gerardo Roza + * @author Joe Grandja + * @since 0.4.0 + * @see AuthenticationConverter + * @see OAuth2TokenIntrospectionAuthenticationToken + * @see OAuth2TokenIntrospectionEndpointFilter + */ +public final class OAuth2TokenIntrospectionAuthenticationConverter implements AuthenticationConverter { + + @Override + public Authentication convert(HttpServletRequest request) { + Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); + + MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); + + // token (REQUIRED) + String token = parameters.getFirst(OAuth2ParameterNames.TOKEN); + if (!StringUtils.hasText(token) || + parameters.get(OAuth2ParameterNames.TOKEN).size() != 1) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN); + } + + // token_type_hint (OPTIONAL) + String tokenTypeHint = parameters.getFirst(OAuth2ParameterNames.TOKEN_TYPE_HINT); + if (StringUtils.hasText(tokenTypeHint) && + parameters.get(OAuth2ParameterNames.TOKEN_TYPE_HINT).size() != 1) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN_TYPE_HINT); + } + + Map additionalParameters = new HashMap<>(); + parameters.forEach((key, value) -> { + if (!key.equals(OAuth2ParameterNames.TOKEN) && + !key.equals(OAuth2ParameterNames.TOKEN_TYPE_HINT)) { + additionalParameters.put(key, value.get(0)); + } + }); + + return new OAuth2TokenIntrospectionAuthenticationToken( + token, clientPrincipal, tokenTypeHint, additionalParameters); + } + + private static void throwError(String errorCode, String parameterName) { + OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Token Introspection Parameter: " + parameterName, + "https://datatracker.ietf.org/doc/html/rfc7662#section-2.1"); + throw new OAuth2AuthenticationException(error); + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2TokenRevocationAuthenticationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2TokenRevocationAuthenticationConverter.java new file mode 100644 index 00000000..4a8ffec6 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2TokenRevocationAuthenticationConverter.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020-2022 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.oauth2.server.authorization.web.authentication; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +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.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +/** + * Attempts to extract a Revoke Token Request from {@link HttpServletRequest} + * and then converts it to an {@link OAuth2TokenRevocationAuthenticationToken} used for authenticating the request. + * + * @author Vivek Babu + * @author Joe Grandja + * @since 0.4.0 + * @see AuthenticationConverter + * @see OAuth2TokenRevocationAuthenticationToken + * @see OAuth2TokenRevocationEndpointFilter + */ +public final class OAuth2TokenRevocationAuthenticationConverter implements AuthenticationConverter { + + @Override + public Authentication convert(HttpServletRequest request) { + Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); + + MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); + + // token (REQUIRED) + String token = parameters.getFirst(OAuth2ParameterNames.TOKEN); + if (!StringUtils.hasText(token) || + parameters.get(OAuth2ParameterNames.TOKEN).size() != 1) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN); + } + + // token_type_hint (OPTIONAL) + String tokenTypeHint = parameters.getFirst(OAuth2ParameterNames.TOKEN_TYPE_HINT); + if (StringUtils.hasText(tokenTypeHint) && + parameters.get(OAuth2ParameterNames.TOKEN_TYPE_HINT).size() != 1) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN_TYPE_HINT); + } + + return new OAuth2TokenRevocationAuthenticationToken(token, clientPrincipal, tokenTypeHint); + } + + private static void throwError(String errorCode, String parameterName) { + OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Token Revocation Parameter: " + parameterName, + "https://datatracker.ietf.org/doc/html/rfc7009#section-2.1"); + throw new OAuth2AuthenticationException(error); + } + +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java index 3fb1939e..efacc025 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java @@ -107,6 +107,7 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Refr import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; @@ -165,7 +166,9 @@ public class OAuth2AuthorizationCodeGrantTests { private static HttpMessageConverter accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); private static AuthenticationConverter authorizationRequestConverter; + private static Consumer> authorizationRequestConvertersConsumer; private static AuthenticationProvider authorizationRequestAuthenticationProvider; + private static Consumer> authorizationRequestAuthenticationProvidersConsumer; private static AuthenticationSuccessHandler authorizationResponseHandler; private static AuthenticationFailureHandler authorizationErrorResponseHandler; private static SecurityContextRepository securityContextRepository; @@ -202,7 +205,9 @@ public class OAuth2AuthorizationCodeGrantTests { .tokenEndpoint("/test/token") .build(); authorizationRequestConverter = mock(AuthenticationConverter.class); + authorizationRequestConvertersConsumer = mock(Consumer.class); authorizationRequestAuthenticationProvider = mock(AuthenticationProvider.class); + authorizationRequestAuthenticationProvidersConsumer = mock(Consumer.class); authorizationResponseHandler = mock(AuthenticationSuccessHandler.class); authorizationErrorResponseHandler = mock(AuthenticationFailureHandler.class); securityContextRepository = spy(new HttpSessionSecurityContextRepository()); @@ -638,7 +643,25 @@ public class OAuth2AuthorizationCodeGrantTests { .andExpect(status().isOk()); verify(authorizationRequestConverter).convert(any()); + + @SuppressWarnings("unchecked") + ArgumentCaptor> authenticationConvertersCaptor = ArgumentCaptor.forClass(List.class); + verify(authorizationRequestConvertersConsumer).accept(authenticationConvertersCaptor.capture()); + List authenticationConverters = authenticationConvertersCaptor.getValue(); + assertThat(authenticationConverters).allMatch((converter) -> + converter == authorizationRequestConverter || + converter instanceof OAuth2AuthorizationCodeRequestAuthenticationConverter); + verify(authorizationRequestAuthenticationProvider).authenticate(eq(authorizationCodeRequestAuthenticationResult)); + + @SuppressWarnings("unchecked") + ArgumentCaptor> authenticationProvidersCaptor = ArgumentCaptor.forClass(List.class); + verify(authorizationRequestAuthenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture()); + List authenticationProviders = authenticationProvidersCaptor.getValue(); + assertThat(authenticationProviders).allMatch((provider) -> + provider == authorizationRequestAuthenticationProvider || + provider instanceof OAuth2AuthorizationCodeRequestAuthenticationProvider); + verify(authorizationResponseHandler).onAuthenticationSuccess(any(), any(), eq(authorizationCodeRequestAuthenticationResult)); } @@ -999,7 +1022,9 @@ public class OAuth2AuthorizationCodeGrantTests { .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint .authorizationRequestConverter(authorizationRequestConverter) + .authorizationRequestConverters(authorizationRequestConvertersConsumer) .authenticationProvider(authorizationRequestAuthenticationProvider) + .authenticationProviders(authorizationRequestAuthenticationProvidersConsumer) .authorizationResponseHandler(authorizationResponseHandler) .errorResponseHandler(authorizationErrorResponseHandler)); RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientCredentialsGrantTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientCredentialsGrantTests.java index ccac2e2c..054e416c 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientCredentialsGrantTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientCredentialsGrantTests.java @@ -21,6 +21,8 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; import java.util.Base64; +import java.util.List; +import java.util.function.Consumer; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -35,6 +37,7 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -60,9 +63,15 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.jose.TestJwks; import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.authentication.ClientSecretAuthenticationProvider; +import org.springframework.security.oauth2.server.authorization.authentication.JwtClientAssertionAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider; +import org.springframework.security.oauth2.server.authorization.authentication.PublicClientAuthenticationProvider; 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; @@ -73,6 +82,13 @@ import org.springframework.security.oauth2.server.authorization.jackson2.Testing import org.springframework.security.oauth2.server.authorization.test.SpringTestRule; import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; +import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretBasicAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretPostAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.JwtClientAssertionAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.PublicClientAuthenticationConverter; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; @@ -81,6 +97,7 @@ import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +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; @@ -104,7 +121,9 @@ public class OAuth2ClientCredentialsGrantTests { private static JWKSource jwkSource; private static OAuth2TokenCustomizer jwtCustomizer; private static AuthenticationConverter authenticationConverter; + private static Consumer> authenticationConvertersConsumer; private static AuthenticationProvider authenticationProvider; + private static Consumer> authenticationProvidersConsumer; private static AuthenticationSuccessHandler authenticationSuccessHandler; private static AuthenticationFailureHandler authenticationFailureHandler; @@ -126,7 +145,9 @@ public class OAuth2ClientCredentialsGrantTests { jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); jwtCustomizer = mock(OAuth2TokenCustomizer.class); authenticationConverter = mock(AuthenticationConverter.class); + authenticationConvertersConsumer = mock(Consumer.class); authenticationProvider = mock(AuthenticationProvider.class); + authenticationProvidersConsumer = mock(Consumer.class); authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class); authenticationFailureHandler = mock(AuthenticationFailureHandler.class); db = new EmbeddedDatabaseBuilder() @@ -143,7 +164,9 @@ public class OAuth2ClientCredentialsGrantTests { public void setup() { reset(jwtCustomizer); reset(authenticationConverter); + reset(authenticationConvertersConsumer); reset(authenticationProvider); + reset(authenticationProvidersConsumer); reset(authenticationSuccessHandler); reset(authenticationFailureHandler); } @@ -234,7 +257,29 @@ public class OAuth2ClientCredentialsGrantTests { .andExpect(status().isOk()); verify(authenticationConverter).convert(any()); + + @SuppressWarnings("unchecked") + ArgumentCaptor> authenticationConvertersCaptor = ArgumentCaptor.forClass(List.class); + verify(authenticationConvertersConsumer).accept(authenticationConvertersCaptor.capture()); + List authenticationConverters = authenticationConvertersCaptor.getValue(); + assertThat(authenticationConverters).allMatch((converter) -> + converter == authenticationConverter || + converter instanceof OAuth2AuthorizationCodeAuthenticationConverter || + converter instanceof OAuth2RefreshTokenAuthenticationConverter || + converter instanceof OAuth2ClientCredentialsAuthenticationConverter); + verify(authenticationProvider).authenticate(eq(clientCredentialsAuthentication)); + + @SuppressWarnings("unchecked") + ArgumentCaptor> authenticationProvidersCaptor = ArgumentCaptor.forClass(List.class); + verify(authenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture()); + List authenticationProviders = authenticationProvidersCaptor.getValue(); + assertThat(authenticationProviders).allMatch((provider) -> + provider == authenticationProvider || + provider instanceof OAuth2AuthorizationCodeAuthenticationProvider || + provider instanceof OAuth2RefreshTokenAuthenticationProvider || + provider instanceof OAuth2ClientCredentialsAuthenticationProvider); + verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), eq(accessTokenAuthentication)); } @@ -246,19 +291,40 @@ public class OAuth2ClientCredentialsGrantTests { this.registeredClientRepository.save(registeredClient); OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( - registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); + registeredClient, new ClientAuthenticationMethod("custom"), null); 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()))) + .param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())) .andExpect(status().isOk()); verify(authenticationConverter).convert(any()); + + @SuppressWarnings("unchecked") + ArgumentCaptor> authenticationConvertersCaptor = ArgumentCaptor.forClass(List.class); + verify(authenticationConvertersConsumer).accept(authenticationConvertersCaptor.capture()); + List authenticationConverters = authenticationConvertersCaptor.getValue(); + assertThat(authenticationConverters).allMatch((converter) -> + converter == authenticationConverter || + converter instanceof JwtClientAssertionAuthenticationConverter || + converter instanceof ClientSecretBasicAuthenticationConverter || + converter instanceof ClientSecretPostAuthenticationConverter || + converter instanceof PublicClientAuthenticationConverter); + verify(authenticationProvider).authenticate(eq(clientPrincipal)); + + @SuppressWarnings("unchecked") + ArgumentCaptor> authenticationProvidersCaptor = ArgumentCaptor.forClass(List.class); + verify(authenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture()); + List authenticationProviders = authenticationProvidersCaptor.getValue(); + assertThat(authenticationProviders).allMatch((provider) -> + provider == authenticationProvider || + provider instanceof JwtClientAssertionAuthenticationProvider || + provider instanceof ClientSecretAuthenticationProvider || + provider instanceof PublicClientAuthenticationProvider); + verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), eq(clientPrincipal)); } @@ -341,7 +407,9 @@ public class OAuth2ClientCredentialsGrantTests { .tokenEndpoint(tokenEndpoint -> tokenEndpoint .accessTokenRequestConverter(authenticationConverter) + .accessTokenRequestConverters(authenticationConvertersConsumer) .authenticationProvider(authenticationProvider) + .authenticationProviders(authenticationProvidersConsumer) .accessTokenResponseHandler(authenticationSuccessHandler) .errorResponseHandler(authenticationFailureHandler)); RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); @@ -371,7 +439,9 @@ public class OAuth2ClientCredentialsGrantTests { .clientAuthentication(clientAuthentication -> clientAuthentication .authenticationConverter(authenticationConverter) + .authenticationConverters(authenticationConvertersConsumer) .authenticationProvider(authenticationProvider) + .authenticationProviders(authenticationProvidersConsumer) .authenticationSuccessHandler(authenticationSuccessHandler) .errorResponseHandler(authenticationFailureHandler)); RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionTests.java index 0a7761b3..120e8287 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionTests.java @@ -25,6 +25,7 @@ import java.util.Base64; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.function.Consumer; import org.junit.After; import org.junit.AfterClass; @@ -72,6 +73,7 @@ import org.springframework.security.oauth2.server.authorization.OAuth2TokenIntro import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; 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.OAuth2TokenIntrospectionAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken; import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper; @@ -88,6 +90,7 @@ import org.springframework.security.oauth2.server.authorization.test.SpringTestR import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsSet; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenIntrospectionAuthenticationConverter; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; @@ -118,7 +121,9 @@ public class OAuth2TokenIntrospectionTests { private static AuthorizationServerSettings authorizationServerSettings; private static OAuth2TokenCustomizer accessTokenCustomizer; private static AuthenticationConverter authenticationConverter; + private static Consumer> authenticationConvertersConsumer; private static AuthenticationProvider authenticationProvider; + private static Consumer> authenticationProvidersConsumer; private static AuthenticationSuccessHandler authenticationSuccessHandler; private static AuthenticationFailureHandler authenticationFailureHandler; private static final HttpMessageConverter tokenIntrospectionHttpResponseConverter = @@ -145,7 +150,9 @@ public class OAuth2TokenIntrospectionTests { public static void init() { authorizationServerSettings = AuthorizationServerSettings.builder().tokenIntrospectionEndpoint("/test/introspect").build(); authenticationConverter = mock(AuthenticationConverter.class); + authenticationConvertersConsumer = mock(Consumer.class); authenticationProvider = mock(AuthenticationProvider.class); + authenticationProvidersConsumer = mock(Consumer.class); authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class); authenticationFailureHandler = mock(AuthenticationFailureHandler.class); accessTokenCustomizer = mock(OAuth2TokenCustomizer.class); @@ -364,7 +371,25 @@ public class OAuth2TokenIntrospectionTests { // @formatter:on verify(authenticationConverter).convert(any()); + + @SuppressWarnings("unchecked") + ArgumentCaptor> authenticationConvertersCaptor = ArgumentCaptor.forClass(List.class); + verify(authenticationConvertersConsumer).accept(authenticationConvertersCaptor.capture()); + List authenticationConverters = authenticationConvertersCaptor.getValue(); + assertThat(authenticationConverters).allMatch((converter) -> + converter == authenticationConverter || + converter instanceof OAuth2TokenIntrospectionAuthenticationConverter); + verify(authenticationProvider).authenticate(eq(tokenIntrospectionAuthentication)); + + @SuppressWarnings("unchecked") + ArgumentCaptor> authenticationProvidersCaptor = ArgumentCaptor.forClass(List.class); + verify(authenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture()); + List authenticationProviders = authenticationProvidersCaptor.getValue(); + assertThat(authenticationProviders).allMatch((provider) -> + provider == authenticationProvider || + provider instanceof OAuth2TokenIntrospectionAuthenticationProvider); + verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), eq(tokenIntrospectionAuthentication)); } @@ -486,7 +511,9 @@ public class OAuth2TokenIntrospectionTests { .tokenIntrospectionEndpoint(tokenIntrospectionEndpoint -> tokenIntrospectionEndpoint .introspectionRequestConverter(authenticationConverter) + .introspectionRequestConverters(authenticationConvertersConsumer) .authenticationProvider(authenticationProvider) + .authenticationProviders(authenticationProvidersConsumer) .introspectionResponseHandler(authenticationSuccessHandler) .errorResponseHandler(authenticationFailureHandler)); RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationTests.java index 7c2cadb3..6dbc63d7 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationTests.java @@ -18,6 +18,8 @@ package org.springframework.security.oauth2.server.authorization.config.annotati import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.List; +import java.util.function.Consumer; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.source.JWKSource; @@ -27,6 +29,7 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -56,6 +59,7 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; 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.OAuth2TokenRevocationAuthenticationProvider; 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; @@ -65,6 +69,7 @@ import org.springframework.security.oauth2.server.authorization.client.TestRegis import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.oauth2.server.authorization.jackson2.TestingAuthenticationTokenMixin; import org.springframework.security.oauth2.server.authorization.test.SpringTestRule; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenRevocationAuthenticationConverter; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; @@ -93,7 +98,9 @@ public class OAuth2TokenRevocationTests { private static EmbeddedDatabase db; private static JWKSource jwkSource; private static AuthenticationConverter authenticationConverter; + private static Consumer> authenticationConvertersConsumer; private static AuthenticationProvider authenticationProvider; + private static Consumer> authenticationProvidersConsumer; private static AuthenticationSuccessHandler authenticationSuccessHandler; private static AuthenticationFailureHandler authenticationFailureHandler; @@ -117,7 +124,9 @@ public class OAuth2TokenRevocationTests { JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK); jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); authenticationConverter = mock(AuthenticationConverter.class); + authenticationConvertersConsumer = mock(Consumer.class); authenticationProvider = mock(AuthenticationProvider.class); + authenticationProvidersConsumer = mock(Consumer.class); authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class); authenticationFailureHandler = mock(AuthenticationFailureHandler.class); db = new EmbeddedDatabaseBuilder() @@ -218,7 +227,25 @@ public class OAuth2TokenRevocationTests { .andExpect(status().isOk()); verify(authenticationConverter).convert(any()); + + @SuppressWarnings("unchecked") + ArgumentCaptor> authenticationConvertersCaptor = ArgumentCaptor.forClass(List.class); + verify(authenticationConvertersConsumer).accept(authenticationConvertersCaptor.capture()); + List authenticationConverters = authenticationConvertersCaptor.getValue(); + assertThat(authenticationConverters).allMatch((converter) -> + converter == authenticationConverter || + converter instanceof OAuth2TokenRevocationAuthenticationConverter); + verify(authenticationProvider).authenticate(eq(tokenRevocationAuthentication)); + + @SuppressWarnings("unchecked") + ArgumentCaptor> authenticationProvidersCaptor = ArgumentCaptor.forClass(List.class); + verify(authenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture()); + List authenticationProviders = authenticationProvidersCaptor.getValue(); + assertThat(authenticationProviders).allMatch((provider) -> + provider == authenticationProvider || + provider instanceof OAuth2TokenRevocationAuthenticationProvider); + verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), eq(tokenRevocationAuthentication)); } @@ -304,7 +331,9 @@ public class OAuth2TokenRevocationTests { .tokenRevocationEndpoint(tokenRevocationEndpoint -> tokenRevocationEndpoint .revocationRequestConverter(authenticationConverter) + .revocationRequestConverters(authenticationConvertersConsumer) .authenticationProvider(authenticationProvider) + .authenticationProviders(authenticationProvidersConsumer) .revocationResponseHandler(authenticationSuccessHandler) .errorResponseHandler(authenticationFailureHandler)); RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); From 1db05991afb75097b2224cdad2a5f64f1b143050 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Fri, 9 Sep 2022 17:33:46 -0400 Subject: [PATCH 8/9] Make OAuth2AuthenticationContext an interface Closes gh-890 --- .../OAuth2AuthenticationContext.java | 55 +-------- ...ationCodeRequestAuthenticationContext.java | 105 ++++++++++++++++++ ...tionCodeRequestAuthenticationProvider.java | 8 +- ...orizationConsentAuthenticationContext.java | 23 +++- .../OidcUserInfoAuthenticationContext.java | 22 +++- 5 files changed, 156 insertions(+), 57 deletions(-) create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationContext.java diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationContext.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationContext.java index e61ac092..58c3cbbe 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationContext.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationContext.java @@ -15,54 +15,24 @@ */ package org.springframework.security.oauth2.server.authorization.authentication; -import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.function.Consumer; -import org.springframework.lang.Nullable; +import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.server.authorization.context.Context; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; /** - * A context that holds an {@link Authentication} and (optionally) additional information. + * A context that holds an {@link Authentication} and (optionally) additional information + * and is used in an {@link AuthenticationProvider}. * * @author Joe Grandja * @since 0.2.0 * @see Context */ -public class OAuth2AuthenticationContext implements Context { - private final Map context; - - /** - * Constructs an {@code OAuth2AuthenticationContext} using the provided parameters. - * - * @param authentication the {@code Authentication} - * @param context a {@code Map} of additional context information - */ - public OAuth2AuthenticationContext(Authentication authentication, @Nullable Map context) { - Assert.notNull(authentication, "authentication cannot be null"); - Map ctx = new HashMap<>(); - if (!CollectionUtils.isEmpty(context)) { - ctx.putAll(context); - } - ctx.put(Authentication.class, authentication); - this.context = Collections.unmodifiableMap(ctx); - } - - /** - * Constructs an {@code OAuth2AuthenticationContext} using the provided parameters. - * - * @param context a {@code Map} of context information, must contain the {@code Authentication} - * @since 0.2.1 - */ - public OAuth2AuthenticationContext(Map context) { - Assert.notEmpty(context, "context cannot be empty"); - Assert.notNull(context.get(Authentication.class), "authentication cannot be null"); - this.context = Collections.unmodifiableMap(new HashMap<>(context)); - } +public interface OAuth2AuthenticationContext extends Context { /** * Returns the {@link Authentication} associated to the context. @@ -71,23 +41,10 @@ public class OAuth2AuthenticationContext implements Context { * @return the {@link Authentication} */ @SuppressWarnings("unchecked") - public T getAuthentication() { + default T getAuthentication() { return (T) get(Authentication.class); } - @SuppressWarnings("unchecked") - @Nullable - @Override - public V get(Object key) { - return hasKey(key) ? (V) this.context.get(key) : null; - } - - @Override - public boolean hasKey(Object key) { - Assert.notNull(key, "key cannot be null"); - return this.context.containsKey(key); - } - /** * A builder for subclasses of {@link OAuth2AuthenticationContext}. * @@ -95,7 +52,7 @@ public class OAuth2AuthenticationContext implements Context { * @param the type of the builder * @since 0.2.1 */ - protected static abstract class AbstractBuilder> { + abstract class AbstractBuilder> { private final Map context = new HashMap<>(); protected AbstractBuilder(Authentication authentication) { diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationContext.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationContext.java new file mode 100644 index 00000000..73d902f3 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationContext.java @@ -0,0 +1,105 @@ +/* + * Copyright 2020-2022 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.oauth2.server.authorization.authentication; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.lang.Nullable; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.util.Assert; + +/** + * An {@link OAuth2AuthenticationContext} that holds an {@link OAuth2AuthorizationCodeRequestAuthenticationToken} and additional information + * and is used when validating the OAuth 2.0 Authorization Request used in the Authorization Code Grant. + * + * @author Joe Grandja + * @since 0.4.0 + * @see OAuth2AuthenticationContext + * @see OAuth2AuthorizationCodeRequestAuthenticationProvider + */ +public final class OAuth2AuthorizationCodeRequestAuthenticationContext implements OAuth2AuthenticationContext { + private final Map context; + + private OAuth2AuthorizationCodeRequestAuthenticationContext(Map context) { + this.context = Collections.unmodifiableMap(new HashMap<>(context)); + } + + @SuppressWarnings("unchecked") + @Nullable + @Override + public V get(Object key) { + return hasKey(key) ? (V) this.context.get(key) : null; + } + + @Override + public boolean hasKey(Object key) { + Assert.notNull(key, "key cannot be null"); + return this.context.containsKey(key); + } + + /** + * Returns the {@link RegisteredClient registered client}. + * + * @return the {@link RegisteredClient} + */ + public RegisteredClient getRegisteredClient() { + return get(RegisteredClient.class); + } + + /** + * Constructs a new {@link Builder} with the provided {@link OAuth2AuthorizationCodeRequestAuthenticationToken}. + * + * @param authentication the {@link OAuth2AuthorizationCodeRequestAuthenticationToken} + * @return the {@link Builder} + */ + public static Builder with(OAuth2AuthorizationCodeRequestAuthenticationToken authentication) { + return new Builder(authentication); + } + + /** + * A builder for {@link OAuth2AuthorizationCodeRequestAuthenticationContext}. + */ + public static final class Builder extends AbstractBuilder { + + private Builder(OAuth2AuthorizationCodeRequestAuthenticationToken authentication) { + super(authentication); + } + + /** + * Sets the {@link RegisteredClient registered client}. + * + * @param registeredClient the {@link RegisteredClient} + * @return the {@link Builder} for further configuration + */ + public Builder registeredClient(RegisteredClient registeredClient) { + return put(RegisteredClient.class, registeredClient); + } + + /** + * Builds a new {@link OAuth2AuthorizationCodeRequestAuthenticationContext}. + * + * @return the {@link OAuth2AuthorizationCodeRequestAuthenticationContext} + */ + public OAuth2AuthorizationCodeRequestAuthenticationContext build() { + Assert.notNull(get(RegisteredClient.class), "registeredClient cannot be null"); + return new OAuth2AuthorizationCodeRequestAuthenticationContext(getContext()); + } + + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java index f549c2b2..dadeb212 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java @@ -186,10 +186,10 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen authorizationCodeRequestAuthentication, null); } - Map context = new HashMap<>(); - context.put(RegisteredClient.class, registeredClient); - OAuth2AuthenticationContext authenticationContext = new OAuth2AuthenticationContext( - authorizationCodeRequestAuthentication, context); + OAuth2AuthenticationContext authenticationContext = + OAuth2AuthorizationCodeRequestAuthenticationContext.with(authorizationCodeRequestAuthentication) + .registeredClient(registeredClient) + .build(); OAuth2AuthenticationValidator redirectUriValidator = resolveAuthenticationValidator(OAuth2ParameterNames.REDIRECT_URI); redirectUriValidator.validate(authenticationContext); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContext.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContext.java index 20bc9393..e9986e60 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContext.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContext.java @@ -15,8 +15,12 @@ */ package org.springframework.security.oauth2.server.authorization.authentication; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; +import java.util.function.Consumer; +import org.springframework.lang.Nullable; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent; @@ -32,11 +36,26 @@ import org.springframework.util.Assert; * @since 0.2.1 * @see OAuth2AuthenticationContext * @see OAuth2AuthorizationConsent + * @see OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthorizationConsentCustomizer(Consumer) */ -public final class OAuth2AuthorizationConsentAuthenticationContext extends OAuth2AuthenticationContext { +public final class OAuth2AuthorizationConsentAuthenticationContext implements OAuth2AuthenticationContext { + private final Map context; private OAuth2AuthorizationConsentAuthenticationContext(Map context) { - super(context); + this.context = Collections.unmodifiableMap(new HashMap<>(context)); + } + + @SuppressWarnings("unchecked") + @Nullable + @Override + public V get(Object key) { + return hasKey(key) ? (V) this.context.get(key) : null; + } + + @Override + public boolean hasKey(Object key) { + Assert.notNull(key, "key cannot be null"); + return this.context.containsKey(key); } /** diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcUserInfoAuthenticationContext.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcUserInfoAuthenticationContext.java index 92f0d164..28cd32a6 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcUserInfoAuthenticationContext.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcUserInfoAuthenticationContext.java @@ -15,9 +15,12 @@ */ package org.springframework.security.oauth2.server.authorization.oidc.authentication; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.function.Function; +import org.springframework.lang.Nullable; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.oidc.OidcUserInfo; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; @@ -31,12 +34,27 @@ import org.springframework.util.Assert; * @author Joe Grandja * @since 0.2.1 * @see OAuth2AuthenticationContext + * @see OidcUserInfo * @see OidcUserInfoAuthenticationProvider#setUserInfoMapper(Function) */ -public final class OidcUserInfoAuthenticationContext extends OAuth2AuthenticationContext { +public final class OidcUserInfoAuthenticationContext implements OAuth2AuthenticationContext { + private final Map context; private OidcUserInfoAuthenticationContext(Map context) { - super(context); + this.context = Collections.unmodifiableMap(new HashMap<>(context)); + } + + @SuppressWarnings("unchecked") + @Nullable + @Override + public V get(Object key) { + return hasKey(key) ? (V) this.context.get(key) : null; + } + + @Override + public boolean hasKey(Object key) { + Assert.notNull(key, "key cannot be null"); + return this.context.containsKey(key); } /** From c326b1a2ba02c8ebee5261c026779ec91c774752 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 12 Sep 2022 10:31:32 -0400 Subject: [PATCH 9/9] Remove OAuth2AuthenticationValidator Closes gh-891 --- .../OAuth2AuthenticationValidator.java | 40 ---- ...ationCodeRequestAuthenticationContext.java | 4 +- ...tionCodeRequestAuthenticationProvider.java | 182 ++------------ ...ionCodeRequestAuthenticationValidator.java | 226 ++++++++++++++++++ ...odeRequestAuthenticationProviderTests.java | 19 +- 5 files changed, 253 insertions(+), 218 deletions(-) delete mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationValidator.java create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationValidator.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationValidator.java deleted file mode 100644 index afcb2510..00000000 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationValidator.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2020-2022 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.oauth2.server.authorization.authentication; - -import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; - -/** - * Implementations of this interface are responsible for validating the attribute(s) - * of the {@link Authentication} associated to the {@link OAuth2AuthenticationContext}. - * - * @author Joe Grandja - * @since 0.2.0 - * @see OAuth2AuthenticationContext - */ -@FunctionalInterface -public interface OAuth2AuthenticationValidator { - - /** - * Validate the attribute(s) of the {@link Authentication}. - * - * @param authenticationContext the authentication context - * @throws OAuth2AuthenticationException if the attribute(s) of the {@code Authentication} is invalid - */ - void validate(OAuth2AuthenticationContext authenticationContext) throws OAuth2AuthenticationException; - -} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationContext.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationContext.java index 73d902f3..c158d940 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationContext.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationContext.java @@ -18,6 +18,7 @@ package org.springframework.security.oauth2.server.authorization.authentication; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.function.Consumer; import org.springframework.lang.Nullable; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; @@ -30,7 +31,8 @@ import org.springframework.util.Assert; * @author Joe Grandja * @since 0.4.0 * @see OAuth2AuthenticationContext - * @see OAuth2AuthorizationCodeRequestAuthenticationProvider + * @see OAuth2AuthorizationCodeRequestAuthenticationToken + * @see OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthenticationValidator(Consumer) */ public final class OAuth2AuthorizationCodeRequestAuthenticationContext implements OAuth2AuthenticationContext { private final Map context; diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java index dadeb212..80c50097 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java @@ -19,12 +19,9 @@ import java.security.Principal; import java.time.Instant; import java.util.Base64; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.Set; import java.util.function.Consumer; -import java.util.function.Function; import org.springframework.lang.Nullable; import org.springframework.security.authentication.AnonymousAuthenticationToken; @@ -55,8 +52,6 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Toke import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import org.springframework.web.util.UriComponents; -import org.springframework.web.util.UriComponentsBuilder; /** * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Request (and Consent) @@ -66,6 +61,7 @@ import org.springframework.web.util.UriComponentsBuilder; * @author Steve Riesenberg * @since 0.1.2 * @see OAuth2AuthorizationCodeRequestAuthenticationToken + * @see OAuth2AuthorizationCodeRequestAuthenticationValidator * @see OAuth2AuthorizationCodeAuthenticationProvider * @see RegisteredClientRepository * @see OAuth2AuthorizationService @@ -78,13 +74,12 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE); private static final StringKeyGenerator DEFAULT_STATE_GENERATOR = new Base64StringKeyGenerator(Base64.getUrlEncoder()); - private static final Function DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER = - createDefaultAuthenticationValidatorResolver(); private final RegisteredClientRepository registeredClientRepository; private final OAuth2AuthorizationService authorizationService; private final OAuth2AuthorizationConsentService authorizationConsentService; private OAuth2TokenGenerator authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator(); - private Function authenticationValidatorResolver = DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER; + private Consumer authenticationValidator = + new OAuth2AuthorizationCodeRequestAuthenticationValidator(); private Consumer authorizationConsentCustomizer; /** @@ -131,23 +126,20 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen } /** - * Sets the resolver that resolves an {@link OAuth2AuthenticationValidator} from the provided OAuth 2.0 Authorization Request parameter. + * Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext} + * and is responsible for validating specific OAuth 2.0 Authorization Request parameters + * associated in the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}. + * The default authentication validator is {@link OAuth2AuthorizationCodeRequestAuthenticationValidator}. * *

- * The following OAuth 2.0 Authorization Request parameters are supported: - *

    - *
  1. {@link OAuth2ParameterNames#REDIRECT_URI}
  2. - *
  3. {@link OAuth2ParameterNames#SCOPE}
  4. - *
+ * NOTE: The authentication validator MUST throw {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails. * - *

- * NOTE: The resolved {@link OAuth2AuthenticationValidator} MUST throw {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails. - * - * @param authenticationValidatorResolver the resolver that resolves an {@link OAuth2AuthenticationValidator} from the provided OAuth 2.0 Authorization Request parameter + * @param authenticationValidator the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext} and is responsible for validating specific OAuth 2.0 Authorization Request parameters + * @since 0.4.0 */ - public void setAuthenticationValidatorResolver(Function authenticationValidatorResolver) { - Assert.notNull(authenticationValidatorResolver, "authenticationValidatorResolver cannot be null"); - this.authenticationValidatorResolver = authenticationValidatorResolver; + public void setAuthenticationValidator(Consumer authenticationValidator) { + Assert.notNull(authenticationValidator, "authenticationValidator cannot be null"); + this.authenticationValidator = authenticationValidator; } /** @@ -186,22 +178,17 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen authorizationCodeRequestAuthentication, null); } - OAuth2AuthenticationContext authenticationContext = + OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext = OAuth2AuthorizationCodeRequestAuthenticationContext.with(authorizationCodeRequestAuthentication) .registeredClient(registeredClient) .build(); - - OAuth2AuthenticationValidator redirectUriValidator = resolveAuthenticationValidator(OAuth2ParameterNames.REDIRECT_URI); - redirectUriValidator.validate(authenticationContext); + this.authenticationValidator.accept(authenticationContext); if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) { throwError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID, authorizationCodeRequestAuthentication, registeredClient); } - OAuth2AuthenticationValidator scopeValidator = resolveAuthenticationValidator(OAuth2ParameterNames.SCOPE); - scopeValidator.validate(authenticationContext); - // code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE) String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE); if (StringUtils.hasText(codeChallenge)) { @@ -284,13 +271,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen .build(); } - private OAuth2AuthenticationValidator resolveAuthenticationValidator(String parameterName) { - OAuth2AuthenticationValidator authenticationValidator = this.authenticationValidatorResolver.apply(parameterName); - return authenticationValidator != null ? - authenticationValidator : - DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER.apply(parameterName); - } - private Authentication authenticateAuthorizationConsent(Authentication authentication) throws AuthenticationException { OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication; @@ -414,13 +394,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen .build(); } - private static Function createDefaultAuthenticationValidatorResolver() { - Map authenticationValidators = new HashMap<>(); - authenticationValidators.put(OAuth2ParameterNames.REDIRECT_URI, new DefaultRedirectUriOAuth2AuthenticationValidator()); - authenticationValidators.put(OAuth2ParameterNames.SCOPE, new DefaultScopeOAuth2AuthenticationValidator()); - return authenticationValidators::get; - } - private static OAuth2Authorization.Builder authorizationBuilder(RegisteredClient registeredClient, Authentication principal, OAuth2AuthorizationRequest authorizationRequest) { return OAuth2Authorization.withRegisteredClient(registeredClient) @@ -505,7 +478,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen boolean redirectOnError = true; if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) && (parameterName.equals(OAuth2ParameterNames.CLIENT_ID) || - parameterName.equals(OAuth2ParameterNames.REDIRECT_URI) || parameterName.equals(OAuth2ParameterNames.STATE))) { redirectOnError = false; } @@ -520,14 +492,14 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen .redirectUri(redirectUri) .state(state) .build(); - authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated()); } else if (!redirectOnError && StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) { authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication) .redirectUri(null) // Prevent redirects .build(); - authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated()); } + authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated()); + throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult); } @@ -569,124 +541,4 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen } - private static class DefaultRedirectUriOAuth2AuthenticationValidator implements OAuth2AuthenticationValidator { - - @Override - public void validate(OAuth2AuthenticationContext authenticationContext) { - OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = - authenticationContext.getAuthentication(); - RegisteredClient registeredClient = authenticationContext.get(RegisteredClient.class); - - String requestedRedirectUri = authorizationCodeRequestAuthentication.getRedirectUri(); - - if (StringUtils.hasText(requestedRedirectUri)) { - // ***** redirect_uri is available in authorization request - - UriComponents requestedRedirect = null; - try { - requestedRedirect = UriComponentsBuilder.fromUriString(requestedRedirectUri).build(); - } catch (Exception ex) { } - if (requestedRedirect == null || requestedRedirect.getFragment() != null) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, - authorizationCodeRequestAuthentication, registeredClient); - } - - String requestedRedirectHost = requestedRedirect.getHost(); - if (requestedRedirectHost == null || requestedRedirectHost.equals("localhost")) { - // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1 - // While redirect URIs using localhost (i.e., "http://localhost:{port}/{path}") - // function similarly to loopback IP redirects described in Section 10.3.3, - // the use of "localhost" is NOT RECOMMENDED. - OAuth2Error error = new OAuth2Error( - OAuth2ErrorCodes.INVALID_REQUEST, - "localhost is not allowed for the redirect_uri (" + requestedRedirectUri + "). " + - "Use the IP literal (127.0.0.1) instead.", - "https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1"); - throwError(error, OAuth2ParameterNames.REDIRECT_URI, - authorizationCodeRequestAuthentication, registeredClient, null); - } - - if (!isLoopbackAddress(requestedRedirectHost)) { - // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7 - // When comparing client redirect URIs against pre-registered URIs, - // authorization servers MUST utilize exact string matching. - if (!registeredClient.getRedirectUris().contains(requestedRedirectUri)) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, - authorizationCodeRequestAuthentication, registeredClient); - } - } else { - // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-10.3.3 - // The authorization server MUST allow any port to be specified at the - // time of the request for loopback IP redirect URIs, to accommodate - // clients that obtain an available ephemeral port from the operating - // system at the time of the request. - boolean validRedirectUri = false; - for (String registeredRedirectUri : registeredClient.getRedirectUris()) { - UriComponentsBuilder registeredRedirect = UriComponentsBuilder.fromUriString(registeredRedirectUri); - registeredRedirect.port(requestedRedirect.getPort()); - if (registeredRedirect.build().toString().equals(requestedRedirect.toString())) { - validRedirectUri = true; - break; - } - } - if (!validRedirectUri) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, - authorizationCodeRequestAuthentication, registeredClient); - } - } - - } else { - // ***** redirect_uri is NOT available in authorization request - - if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID) || - registeredClient.getRedirectUris().size() != 1) { - // redirect_uri is REQUIRED for OpenID Connect - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, - authorizationCodeRequestAuthentication, registeredClient); - } - } - } - - private static boolean isLoopbackAddress(String host) { - // IPv6 loopback address should either be "0:0:0:0:0:0:0:1" or "::1" - if ("[0:0:0:0:0:0:0:1]".equals(host) || "[::1]".equals(host)) { - return true; - } - // IPv4 loopback address ranges from 127.0.0.1 to 127.255.255.255 - String[] ipv4Octets = host.split("\\."); - if (ipv4Octets.length != 4) { - return false; - } - try { - int[] address = new int[ipv4Octets.length]; - for (int i=0; i < ipv4Octets.length; i++) { - address[i] = Integer.parseInt(ipv4Octets[i]); - } - return address[0] == 127 && address[1] >= 0 && address[1] <= 255 && address[2] >= 0 && - address[2] <= 255 && address[3] >= 1 && address[3] <= 255; - } catch (NumberFormatException ex) { - return false; - } - } - - } - - private static class DefaultScopeOAuth2AuthenticationValidator implements OAuth2AuthenticationValidator { - - @Override - public void validate(OAuth2AuthenticationContext authenticationContext) { - OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = - authenticationContext.getAuthentication(); - RegisteredClient registeredClient = authenticationContext.get(RegisteredClient.class); - - Set requestedScopes = authorizationCodeRequestAuthentication.getScopes(); - Set allowedScopes = registeredClient.getScopes(); - if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) { - throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE, - authorizationCodeRequestAuthentication, registeredClient); - } - } - - } - } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java new file mode 100644 index 00000000..c19f6a97 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java @@ -0,0 +1,226 @@ +/* + * Copyright 2020-2022 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.oauth2.server.authorization.authentication; + +import java.util.Set; +import java.util.function.Consumer; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * A {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext} + * containing an {@link OAuth2AuthorizationCodeRequestAuthenticationToken} + * and is the default {@link OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthenticationValidator(Consumer) authentication validator} + * used for validating specific OAuth 2.0 Authorization Request parameters used in the Authorization Code Grant. + * + *

+ * The default implementation first validates {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getRedirectUri()} + * and then {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getScopes()}. + * If validation fails, an {@link OAuth2AuthorizationCodeRequestAuthenticationException} is thrown. + * + * @author Joe Grandja + * @since 0.4.0 + * @see OAuth2AuthorizationCodeRequestAuthenticationContext + * @see OAuth2AuthorizationCodeRequestAuthenticationToken + * @see OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthenticationValidator(Consumer) + */ +public final class OAuth2AuthorizationCodeRequestAuthenticationValidator implements Consumer { + private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1"; + + /** + * The default validator for {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getScopes()}. + */ + public static final Consumer DEFAULT_SCOPE_VALIDATOR = + OAuth2AuthorizationCodeRequestAuthenticationValidator::validateScope; + + /** + * The default validator for {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getRedirectUri()}. + */ + public static final Consumer DEFAULT_REDIRECT_URI_VALIDATOR = + OAuth2AuthorizationCodeRequestAuthenticationValidator::validateRedirectUri; + + private final Consumer authenticationValidator = + DEFAULT_REDIRECT_URI_VALIDATOR.andThen(DEFAULT_SCOPE_VALIDATOR); + + @Override + public void accept(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) { + this.authenticationValidator.accept(authenticationContext); + } + + private static void validateScope(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) { + OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = + authenticationContext.getAuthentication(); + RegisteredClient registeredClient = authenticationContext.getRegisteredClient(); + + Set requestedScopes = authorizationCodeRequestAuthentication.getScopes(); + Set allowedScopes = registeredClient.getScopes(); + if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) { + throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE, + authorizationCodeRequestAuthentication, registeredClient); + } + } + + private static void validateRedirectUri(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) { + OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = + authenticationContext.getAuthentication(); + RegisteredClient registeredClient = authenticationContext.getRegisteredClient(); + + String requestedRedirectUri = authorizationCodeRequestAuthentication.getRedirectUri(); + + if (StringUtils.hasText(requestedRedirectUri)) { + // ***** redirect_uri is available in authorization request + + UriComponents requestedRedirect = null; + try { + requestedRedirect = UriComponentsBuilder.fromUriString(requestedRedirectUri).build(); + } catch (Exception ex) { } + if (requestedRedirect == null || requestedRedirect.getFragment() != null) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, + authorizationCodeRequestAuthentication, registeredClient); + } + + String requestedRedirectHost = requestedRedirect.getHost(); + if (requestedRedirectHost == null || requestedRedirectHost.equals("localhost")) { + // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1 + // While redirect URIs using localhost (i.e., "http://localhost:{port}/{path}") + // function similarly to loopback IP redirects described in Section 10.3.3, + // the use of "localhost" is NOT RECOMMENDED. + OAuth2Error error = new OAuth2Error( + OAuth2ErrorCodes.INVALID_REQUEST, + "localhost is not allowed for the redirect_uri (" + requestedRedirectUri + "). " + + "Use the IP literal (127.0.0.1) instead.", + "https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1"); + throwError(error, OAuth2ParameterNames.REDIRECT_URI, + authorizationCodeRequestAuthentication, registeredClient); + } + + if (!isLoopbackAddress(requestedRedirectHost)) { + // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7 + // When comparing client redirect URIs against pre-registered URIs, + // authorization servers MUST utilize exact string matching. + if (!registeredClient.getRedirectUris().contains(requestedRedirectUri)) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, + authorizationCodeRequestAuthentication, registeredClient); + } + } else { + // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-10.3.3 + // The authorization server MUST allow any port to be specified at the + // time of the request for loopback IP redirect URIs, to accommodate + // clients that obtain an available ephemeral port from the operating + // system at the time of the request. + boolean validRedirectUri = false; + for (String registeredRedirectUri : registeredClient.getRedirectUris()) { + UriComponentsBuilder registeredRedirect = UriComponentsBuilder.fromUriString(registeredRedirectUri); + registeredRedirect.port(requestedRedirect.getPort()); + if (registeredRedirect.build().toString().equals(requestedRedirect.toString())) { + validRedirectUri = true; + break; + } + } + if (!validRedirectUri) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, + authorizationCodeRequestAuthentication, registeredClient); + } + } + + } else { + // ***** redirect_uri is NOT available in authorization request + + if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID) || + registeredClient.getRedirectUris().size() != 1) { + // redirect_uri is REQUIRED for OpenID Connect + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, + authorizationCodeRequestAuthentication, registeredClient); + } + } + } + + private static boolean isLoopbackAddress(String host) { + // IPv6 loopback address should either be "0:0:0:0:0:0:0:1" or "::1" + if ("[0:0:0:0:0:0:0:1]".equals(host) || "[::1]".equals(host)) { + return true; + } + // IPv4 loopback address ranges from 127.0.0.1 to 127.255.255.255 + String[] ipv4Octets = host.split("\\."); + if (ipv4Octets.length != 4) { + return false; + } + try { + int[] address = new int[ipv4Octets.length]; + for (int i=0; i < ipv4Octets.length; i++) { + address[i] = Integer.parseInt(ipv4Octets[i]); + } + return address[0] == 127 && address[1] >= 0 && address[1] <= 255 && address[2] >= 0 && + address[2] <= 255 && address[3] >= 1 && address[3] <= 255; + } catch (NumberFormatException ex) { + return false; + } + } + + private static void throwError(String errorCode, String parameterName, + OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication, + RegisteredClient registeredClient) { + OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, ERROR_URI); + throwError(error, parameterName, authorizationCodeRequestAuthentication, registeredClient); + } + + private static void throwError(OAuth2Error error, String parameterName, + OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication, + RegisteredClient registeredClient) { + + boolean redirectOnError = true; + if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) && + parameterName.equals(OAuth2ParameterNames.REDIRECT_URI)) { + redirectOnError = false; + } + + OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = authorizationCodeRequestAuthentication; + + if (redirectOnError && !StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) { + String redirectUri = registeredClient.getRedirectUris().iterator().next(); + authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication) + .redirectUri(redirectUri) + .build(); + } else if (!redirectOnError && StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) { + authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication) + .redirectUri(null) // Prevent redirects + .build(); + } + + authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated()); + + throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult); + } + + private static OAuth2AuthorizationCodeRequestAuthenticationToken.Builder from(OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) { + return OAuth2AuthorizationCodeRequestAuthenticationToken.with(authorizationCodeRequestAuthentication.getClientId(), (Authentication) authorizationCodeRequestAuthentication.getPrincipal()) + .authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri()) + .redirectUri(authorizationCodeRequestAuthentication.getRedirectUri()) + .scopes(authorizationCodeRequestAuthentication.getScopes()) + .state(authorizationCodeRequestAuthentication.getState()) + .additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters()) + .authorizationCode(authorizationCodeRequestAuthentication.getAuthorizationCode()); + } + +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java index f86d2283..acaa546d 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java @@ -22,7 +22,6 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.function.Consumer; -import java.util.function.Function; import org.junit.Before; import org.junit.Test; @@ -60,7 +59,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -128,10 +126,10 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { } @Test - public void setAuthenticationValidatorResolverWhenNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> this.authenticationProvider.setAuthenticationValidatorResolver(null)) + public void setAuthenticationValidatorWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authenticationProvider.setAuthenticationValidator(null)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("authenticationValidatorResolver cannot be null"); + .hasMessage("authenticationValidator cannot be null"); } @Test @@ -555,14 +553,14 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { } @Test - public void authenticateWhenCustomAuthenticationValidatorResolverThenUsed() { + public void authenticateWhenCustomAuthenticationValidatorThenUsed() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) .thenReturn(registeredClient); @SuppressWarnings("unchecked") - Function authenticationValidatorResolver = mock(Function.class); - this.authenticationProvider.setAuthenticationValidatorResolver(authenticationValidatorResolver); + Consumer authenticationValidator = mock(Consumer.class); + this.authenticationProvider.setAuthenticationValidator(authenticationValidator); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = authorizationCodeRequestAuthentication(registeredClient, this.principal) @@ -573,10 +571,7 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { assertAuthorizationCodeRequestWithAuthorizationCodeResult(registeredClient, authentication, authenticationResult); - ArgumentCaptor parameterNameCaptor = ArgumentCaptor.forClass(String.class); - verify(authenticationValidatorResolver, times(2)).apply(parameterNameCaptor.capture()); - assertThat(parameterNameCaptor.getAllValues()).containsExactly( - OAuth2ParameterNames.REDIRECT_URI, OAuth2ParameterNames.SCOPE); + verify(authenticationValidator).accept(any()); } private void assertAuthorizationCodeRequestWithAuthorizationCodeResult(