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();