浏览代码

Merge branch 0.4.x into main

The following commits are merged using the default merge strategy.

70d433a45aec7128084f5127904e8d070a5e14cd Update ref-doc with OAuth2Authorization.getAuthorizedScopes()
0994a1e1e14263d106373e3397f8c5c44f9a7708 Allow customizing OIDC Provider Configuration Response
8043b8c9492106cede23c97b347b027b375300d0 Allow customizing Authorization Server Metadata Response
4466cbe69d9ce06d042a8cd1aecc1eb4931505c9 Use configured ID Token signature algorithm
502fa24cfb0a015184678157ac449b38e8296883 Polish gh-787
07d69cbfb4447f619f59171fa135afc9620e8e5e Validate client secret not expired
2cc603c7e708290f9ffbd57c865d89e6caa87d6b Improve configurability for AuthenticationConverter and AuthenticationProvider
1db05991afb75097b2224cdad2a5f64f1b143050 Make OAuth2AuthenticationContext an interface
c326b1a2ba02c8ebee5261c026779ec91c774752 Remove OAuth2AuthenticationValidator
Joe Grandja 2 年之前
父节点
当前提交
d184363591
共有 38 个文件被更改,包括 1653 次插入577 次删除
  1. 11 7
      docs/src/docs/asciidoc/configuration-model.adoc
  2. 6 4
      docs/src/docs/asciidoc/core-model-components.adoc
  3. 100 38
      docs/src/docs/asciidoc/protocol-endpoints.adoc
  4. 7 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/ClientSecretAuthenticationProvider.java
  5. 6 49
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationContext.java
  6. 0 40
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationValidator.java
  7. 107 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationContext.java
  8. 20 168
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java
  9. 226 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java
  10. 21 2
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContext.java
  11. 62 11
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationEndpointConfigurer.java
  12. 25 23
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java
  13. 108 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataEndpointConfigurer.java
  14. 68 11
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java
  15. 67 13
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java
  16. 64 13
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionEndpointConfigurer.java
  17. 65 14
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationEndpointConfigurer.java
  18. 19 32
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java
  19. 108 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationEndpointConfigurer.java
  20. 20 2
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcUserInfoAuthenticationContext.java
  21. 20 4
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java
  22. 6 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java
  23. 20 4
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java
  24. 3 51
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilter.java
  25. 5 40
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenRevocationEndpointFilter.java
  26. 86 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2TokenIntrospectionAuthenticationConverter.java
  27. 74 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2TokenRevocationAuthenticationConverter.java
  28. 22 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/ClientSecretAuthenticationProviderTests.java
  29. 7 12
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java
  30. 25 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java
  31. 56 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataTests.java
  32. 74 4
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientCredentialsGrantTests.java
  33. 27 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionTests.java
  34. 29 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationTests.java
  35. 52 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java
  36. 14 14
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java
  37. 9 2
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java
  38. 14 18
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java

+ 11 - 7
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.

+ 6 - 4
docs/src/docs/asciidoc/core-model-components.adoc

@@ -163,8 +163,9 @@ public class OAuth2Authorization implements Serializable {
 	private String registeredClientId;  <2>
 	private String principalName;   <3>
 	private AuthorizationGrantType authorizationGrantType;  <4>
-	private Map<Class<? extends OAuth2Token>, Token<?>> tokens; <5>
-	private Map<String, Object> attributes; <6>
+	private Set<String> authorizedScopes;   <5>
+	private Map<Class<? extends OAuth2Token>, Token<?>> tokens; <6>
+	private Map<String, Object> attributes; <7>
 
 	...
 
@@ -174,8 +175,9 @@ public class OAuth2Authorization implements Serializable {
 <2> `registeredClientId`: The ID that uniquely identifies the <<registered-client, RegisteredClient>>.
 <3> `principalName`: The principal name of the resource owner (or client).
 <4> `authorizationGrantType`: The `AuthorizationGrantType` used.
-<5> `tokens`: The `OAuth2Token` instances (and associated metadata) specific to the executed authorization grant type.
-<6> `attributes`: The additional attributes specific to the executed authorization grant type – for example, the authenticated `Principal`, `OAuth2AuthorizationRequest`, authorized scope(s), and others.
+<5> `authorizedScopes`: The `Set` of scope(s) authorized for the client.
+<6> `tokens`: The `OAuth2Token` instances (and associated metadata) specific to the executed authorization grant type.
+<7> `attributes`: The additional attributes specific to the executed authorization grant type – for example, the authenticated `Principal`, `OAuth2AuthorizationRequest`, and others.
 
 `OAuth2Authorization` and its associated `OAuth2Token` instances have a set lifespan.
 A newly issued `OAuth2Token` is active and becomes inactive when it either expires or is invalidated (revoked).

+ 100 - 38
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.
@@ -179,10 +195,31 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
 [[oauth2-authorization-server-metadata-endpoint]]
 == OAuth2 Authorization Server Metadata Endpoint
 
-`OAuth2AuthorizationServerConfigurer` provides support for the https://datatracker.ietf.org/doc/html/rfc8414#section-3[OAuth2 Authorization Server Metadata endpoint].
+`OAuth2AuthorizationServerMetadataEndpointConfigurer` provides the ability to customize the https://datatracker.ietf.org/doc/html/rfc8414#section-3[OAuth2 Authorization Server Metadata endpoint].
+It defines an extension point that lets you customize the https://datatracker.ietf.org/doc/html/rfc8414#section-3.2[OAuth2 Authorization Server Metadata response].
 
-`OAuth2AuthorizationServerConfigurer` configures the `OAuth2AuthorizationServerMetadataEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
-`OAuth2AuthorizationServerMetadataEndpointFilter` is the `Filter` that processes https://datatracker.ietf.org/doc/html/rfc8414#section-3.1[OAuth2 authorization server metadata requests] and returns the https://datatracker.ietf.org/doc/html/rfc8414#section-3.2[OAuth2AuthorizationServerMetadata response].
+`OAuth2AuthorizationServerMetadataEndpointConfigurer` provides the following configuration option:
+
+[source,java]
+----
+@Bean
+public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
+	OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
+		new OAuth2AuthorizationServerConfigurer();
+	http.apply(authorizationServerConfigurer);
+
+	authorizationServerConfigurer
+		.authorizationServerMetadataEndpoint(authorizationServerMetadataEndpoint ->
+			authorizationServerMetadataEndpoint
+				.authorizationServerMetadataCustomizer(authorizationServerMetadataCustomizer));   <1>
+
+	return http.build();
+}
+----
+<1> `authorizationServerMetadataCustomizer()`: The `Consumer` providing access to the `OAuth2AuthorizationServerMetadata.Builder` allowing the ability to customize the claims of the Authorization Server's configuration.
+
+`OAuth2AuthorizationServerMetadataEndpointConfigurer` configures the `OAuth2AuthorizationServerMetadataEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
+`OAuth2AuthorizationServerMetadataEndpointFilter` is the `Filter` that returns the https://datatracker.ietf.org/doc/html/rfc8414#section-3.2[OAuth2AuthorizationServerMetadata response].
 
 [[jwk-set-endpoint]]
 == JWK Set Endpoint
@@ -198,9 +235,34 @@ The JWK Set endpoint is configured *only* if a `JWKSource<SecurityContext>` `@Be
 [[oidc-provider-configuration-endpoint]]
 == OpenID Connect 1.0 Provider Configuration Endpoint
 
-`OidcConfigurer` provides support for the https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[OpenID Connect 1.0 Provider Configuration endpoint].
+`OidcProviderConfigurationEndpointConfigurer` provides the ability to customize the https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[OpenID Connect 1.0 Provider Configuration endpoint].
+It defines an extension point that lets you customize the https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse[OpenID Provider Configuration response].
+
+`OidcProviderConfigurationEndpointConfigurer` provides the following configuration option:
+
+[source,java]
+----
+@Bean
+public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
+	OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
+		new OAuth2AuthorizationServerConfigurer();
+	http.apply(authorizationServerConfigurer);
+
+	authorizationServerConfigurer
+		.oidc(oidc ->
+			oidc
+				.providerConfigurationEndpoint(providerConfigurationEndpoint ->
+					providerConfigurationEndpoint
+						.providerConfigurationCustomizer(providerConfigurationCustomizer)   <1>
+				)
+		);
+
+	return http.build();
+}
+----
+<1> `providerConfigurationCustomizer()`: The `Consumer` providing access to the `OidcProviderConfiguration.Builder` allowing the ability to customize the claims of the OpenID Provider's configuration.
 
-`OidcConfigurer` configures the `OidcProviderConfigurationEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
+`OidcProviderConfigurationEndpointConfigurer` configures the `OidcProviderConfigurationEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
 `OidcProviderConfigurationEndpointFilter` is the `Filter` that returns the https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse[OidcProviderConfiguration response].
 
 [[oidc-user-info-endpoint]]

+ 7 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/ClientSecretAuthenticationProvider.java

@@ -15,6 +15,8 @@
  */
 package org.springframework.security.oauth2.server.authorization.authentication;
 
+import java.time.Instant;
+
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
@@ -107,6 +109,11 @@ public final class ClientSecretAuthenticationProvider implements AuthenticationP
 			throwInvalidClient(OAuth2ParameterNames.CLIENT_SECRET);
 		}
 
+		if (registeredClient.getClientSecretExpiresAt() != null &&
+				Instant.now().isAfter(registeredClient.getClientSecretExpiresAt())) {
+			throwInvalidClient("client_secret_expires_at");
+		}
+
 		// Validate the "code_verifier" parameter for the confidential client, if available
 		this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);
 

+ 6 - 49
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationContext.java

@@ -15,54 +15,24 @@
  */
 package org.springframework.security.oauth2.server.authorization.authentication;
 
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.function.Consumer;
 
-import org.springframework.lang.Nullable;
+import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.oauth2.server.authorization.context.Context;
 import org.springframework.util.Assert;
-import org.springframework.util.CollectionUtils;
 
 /**
- * A context that holds an {@link Authentication} and (optionally) additional information.
+ * A context that holds an {@link Authentication} and (optionally) additional information
+ * and is used in an {@link AuthenticationProvider}.
  *
  * @author Joe Grandja
  * @since 0.2.0
  * @see Context
  */
-public class OAuth2AuthenticationContext implements Context {
-	private final Map<Object, Object> context;
-
-	/**
-	 * Constructs an {@code OAuth2AuthenticationContext} using the provided parameters.
-	 *
-	 * @param authentication the {@code Authentication}
-	 * @param context a {@code Map} of additional context information
-	 */
-	public OAuth2AuthenticationContext(Authentication authentication, @Nullable Map<Object, Object> context) {
-		Assert.notNull(authentication, "authentication cannot be null");
-		Map<Object, Object> ctx = new HashMap<>();
-		if (!CollectionUtils.isEmpty(context)) {
-			ctx.putAll(context);
-		}
-		ctx.put(Authentication.class, authentication);
-		this.context = Collections.unmodifiableMap(ctx);
-	}
-
-	/**
-	 * Constructs an {@code OAuth2AuthenticationContext} using the provided parameters.
-	 *
-	 * @param context a {@code Map} of context information, must contain the {@code Authentication}
-	 * @since 0.2.1
-	 */
-	public OAuth2AuthenticationContext(Map<Object, Object> context) {
-		Assert.notEmpty(context, "context cannot be empty");
-		Assert.notNull(context.get(Authentication.class), "authentication cannot be null");
-		this.context = Collections.unmodifiableMap(new HashMap<>(context));
-	}
+public interface OAuth2AuthenticationContext extends Context {
 
 	/**
 	 * Returns the {@link Authentication} associated to the context.
@@ -71,23 +41,10 @@ public class OAuth2AuthenticationContext implements Context {
 	 * @return the {@link Authentication}
 	 */
 	@SuppressWarnings("unchecked")
-	public <T extends Authentication> T getAuthentication() {
+	default <T extends Authentication> T getAuthentication() {
 		return (T) get(Authentication.class);
 	}
 
-	@SuppressWarnings("unchecked")
-	@Nullable
-	@Override
-	public <V> V get(Object key) {
-		return hasKey(key) ? (V) this.context.get(key) : null;
-	}
-
-	@Override
-	public boolean hasKey(Object key) {
-		Assert.notNull(key, "key cannot be null");
-		return this.context.containsKey(key);
-	}
-
 	/**
 	 * A builder for subclasses of {@link OAuth2AuthenticationContext}.
 	 *
@@ -95,7 +52,7 @@ public class OAuth2AuthenticationContext implements Context {
 	 * @param <B> the type of the builder
 	 * @since 0.2.1
 	 */
-	protected static abstract class AbstractBuilder<T extends OAuth2AuthenticationContext, B extends AbstractBuilder<T, B>> {
+	abstract class AbstractBuilder<T extends OAuth2AuthenticationContext, B extends AbstractBuilder<T, B>> {
 		private final Map<Object, Object> context = new HashMap<>();
 
 		protected AbstractBuilder(Authentication authentication) {

+ 0 - 40
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationValidator.java

@@ -1,40 +0,0 @@
-/*
- * Copyright 2020-2022 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.springframework.security.oauth2.server.authorization.authentication;
-
-import org.springframework.security.core.Authentication;
-import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
-
-/**
- * Implementations of this interface are responsible for validating the attribute(s)
- * of the {@link Authentication} associated to the {@link OAuth2AuthenticationContext}.
- *
- * @author Joe Grandja
- * @since 0.2.0
- * @see OAuth2AuthenticationContext
- */
-@FunctionalInterface
-public interface OAuth2AuthenticationValidator {
-
-	/**
-	 * Validate the attribute(s) of the {@link Authentication}.
-	 *
-	 * @param authenticationContext the authentication context
-	 * @throws OAuth2AuthenticationException if the attribute(s) of the {@code Authentication} is invalid
-	 */
-	void validate(OAuth2AuthenticationContext authenticationContext) throws OAuth2AuthenticationException;
-
-}

+ 107 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationContext.java

@@ -0,0 +1,107 @@
+/*
+ * Copyright 2020-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization.authentication;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link OAuth2AuthenticationContext} that holds an {@link OAuth2AuthorizationCodeRequestAuthenticationToken} and additional information
+ * and is used when validating the OAuth 2.0 Authorization Request used in the Authorization Code Grant.
+ *
+ * @author Joe Grandja
+ * @since 0.4.0
+ * @see OAuth2AuthenticationContext
+ * @see OAuth2AuthorizationCodeRequestAuthenticationToken
+ * @see OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthenticationValidator(Consumer)
+ */
+public final class OAuth2AuthorizationCodeRequestAuthenticationContext implements OAuth2AuthenticationContext {
+	private final Map<Object, Object> context;
+
+	private OAuth2AuthorizationCodeRequestAuthenticationContext(Map<Object, Object> context) {
+		this.context = Collections.unmodifiableMap(new HashMap<>(context));
+	}
+
+	@SuppressWarnings("unchecked")
+	@Nullable
+	@Override
+	public <V> V get(Object key) {
+		return hasKey(key) ? (V) this.context.get(key) : null;
+	}
+
+	@Override
+	public boolean hasKey(Object key) {
+		Assert.notNull(key, "key cannot be null");
+		return this.context.containsKey(key);
+	}
+
+	/**
+	 * Returns the {@link RegisteredClient registered client}.
+	 *
+	 * @return the {@link RegisteredClient}
+	 */
+	public RegisteredClient getRegisteredClient() {
+		return get(RegisteredClient.class);
+	}
+
+	/**
+	 * Constructs a new {@link Builder} with the provided {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
+	 *
+	 * @param authentication the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
+	 * @return the {@link Builder}
+	 */
+	public static Builder with(OAuth2AuthorizationCodeRequestAuthenticationToken authentication) {
+		return new Builder(authentication);
+	}
+
+	/**
+	 * A builder for {@link OAuth2AuthorizationCodeRequestAuthenticationContext}.
+	 */
+	public static final class Builder extends AbstractBuilder<OAuth2AuthorizationCodeRequestAuthenticationContext, Builder> {
+
+		private Builder(OAuth2AuthorizationCodeRequestAuthenticationToken authentication) {
+			super(authentication);
+		}
+
+		/**
+		 * Sets the {@link RegisteredClient registered client}.
+		 *
+		 * @param registeredClient the {@link RegisteredClient}
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder registeredClient(RegisteredClient registeredClient) {
+			return put(RegisteredClient.class, registeredClient);
+		}
+
+		/**
+		 * Builds a new {@link OAuth2AuthorizationCodeRequestAuthenticationContext}.
+		 *
+		 * @return the {@link OAuth2AuthorizationCodeRequestAuthenticationContext}
+		 */
+		public OAuth2AuthorizationCodeRequestAuthenticationContext build() {
+			Assert.notNull(get(RegisteredClient.class), "registeredClient cannot be null");
+			return new OAuth2AuthorizationCodeRequestAuthenticationContext(getContext());
+		}
+
+	}
+
+}

+ 20 - 168
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java

@@ -19,12 +19,9 @@ import java.security.Principal;
 import java.time.Instant;
 import java.util.Base64;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
-import java.util.function.Function;
 
 import org.springframework.lang.Nullable;
 import org.springframework.security.authentication.AnonymousAuthenticationToken;
@@ -55,8 +52,6 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Toke
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
-import org.springframework.web.util.UriComponents;
-import org.springframework.web.util.UriComponentsBuilder;
 
 /**
  * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Request (and Consent)
@@ -66,6 +61,7 @@ import org.springframework.web.util.UriComponentsBuilder;
  * @author Steve Riesenberg
  * @since 0.1.2
  * @see OAuth2AuthorizationCodeRequestAuthenticationToken
+ * @see OAuth2AuthorizationCodeRequestAuthenticationValidator
  * @see OAuth2AuthorizationCodeAuthenticationProvider
  * @see RegisteredClientRepository
  * @see OAuth2AuthorizationService
@@ -78,13 +74,12 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 	private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
 	private static final StringKeyGenerator DEFAULT_STATE_GENERATOR =
 			new Base64StringKeyGenerator(Base64.getUrlEncoder());
-	private static final Function<String, OAuth2AuthenticationValidator> DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER =
-			createDefaultAuthenticationValidatorResolver();
 	private final RegisteredClientRepository registeredClientRepository;
 	private final OAuth2AuthorizationService authorizationService;
 	private final OAuth2AuthorizationConsentService authorizationConsentService;
 	private OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator();
-	private Function<String, OAuth2AuthenticationValidator> authenticationValidatorResolver = DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER;
+	private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator =
+			new OAuth2AuthorizationCodeRequestAuthenticationValidator();
 	private Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer;
 
 	/**
@@ -131,23 +126,20 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 	}
 
 	/**
-	 * Sets the resolver that resolves an {@link OAuth2AuthenticationValidator} from the provided OAuth 2.0 Authorization Request parameter.
+	 * Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext}
+	 * and is responsible for validating specific OAuth 2.0 Authorization Request parameters
+	 * associated in the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
+	 * The default authentication validator is {@link OAuth2AuthorizationCodeRequestAuthenticationValidator}.
 	 *
 	 * <p>
-	 * The following OAuth 2.0 Authorization Request parameters are supported:
-	 * <ol>
-	 * <li>{@link OAuth2ParameterNames#REDIRECT_URI}</li>
-	 * <li>{@link OAuth2ParameterNames#SCOPE}</li>
-	 * </ol>
+	 * <b>NOTE:</b> The authentication validator MUST throw {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails.
 	 *
-	 * <p>
-	 * <b>NOTE:</b> The resolved {@link OAuth2AuthenticationValidator} MUST throw {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails.
-	 *
-	 * @param authenticationValidatorResolver the resolver that resolves an {@link OAuth2AuthenticationValidator} from the provided OAuth 2.0 Authorization Request parameter
+	 * @param authenticationValidator the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext} and is responsible for validating specific OAuth 2.0 Authorization Request parameters
+	 * @since 0.4.0
 	 */
-	public void setAuthenticationValidatorResolver(Function<String, OAuth2AuthenticationValidator> authenticationValidatorResolver) {
-		Assert.notNull(authenticationValidatorResolver, "authenticationValidatorResolver cannot be null");
-		this.authenticationValidatorResolver = authenticationValidatorResolver;
+	public void setAuthenticationValidator(Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) {
+		Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
+		this.authenticationValidator = authenticationValidator;
 	}
 
 	/**
@@ -186,22 +178,17 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 					authorizationCodeRequestAuthentication, null);
 		}
 
-		Map<Object, Object> context = new HashMap<>();
-		context.put(RegisteredClient.class, registeredClient);
-		OAuth2AuthenticationContext authenticationContext = new OAuth2AuthenticationContext(
-				authorizationCodeRequestAuthentication, context);
-
-		OAuth2AuthenticationValidator redirectUriValidator = resolveAuthenticationValidator(OAuth2ParameterNames.REDIRECT_URI);
-		redirectUriValidator.validate(authenticationContext);
+		OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext =
+				OAuth2AuthorizationCodeRequestAuthenticationContext.with(authorizationCodeRequestAuthentication)
+						.registeredClient(registeredClient)
+						.build();
+		this.authenticationValidator.accept(authenticationContext);
 
 		if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
 			throwError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID,
 					authorizationCodeRequestAuthentication, registeredClient);
 		}
 
-		OAuth2AuthenticationValidator scopeValidator = resolveAuthenticationValidator(OAuth2ParameterNames.SCOPE);
-		scopeValidator.validate(authenticationContext);
-
 		// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
 		String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE);
 		if (StringUtils.hasText(codeChallenge)) {
@@ -284,13 +271,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 				.build();
 	}
 
-	private OAuth2AuthenticationValidator resolveAuthenticationValidator(String parameterName) {
-		OAuth2AuthenticationValidator authenticationValidator = this.authenticationValidatorResolver.apply(parameterName);
-		return authenticationValidator != null ?
-				authenticationValidator :
-				DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER.apply(parameterName);
-	}
-
 	private Authentication authenticateAuthorizationConsent(Authentication authentication) throws AuthenticationException {
 		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
 				(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
@@ -414,13 +394,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 				.build();
 	}
 
-	private static Function<String, OAuth2AuthenticationValidator> createDefaultAuthenticationValidatorResolver() {
-		Map<String, OAuth2AuthenticationValidator> authenticationValidators = new HashMap<>();
-		authenticationValidators.put(OAuth2ParameterNames.REDIRECT_URI, new DefaultRedirectUriOAuth2AuthenticationValidator());
-		authenticationValidators.put(OAuth2ParameterNames.SCOPE, new DefaultScopeOAuth2AuthenticationValidator());
-		return authenticationValidators::get;
-	}
-
 	private static OAuth2Authorization.Builder authorizationBuilder(RegisteredClient registeredClient, Authentication principal,
 			OAuth2AuthorizationRequest authorizationRequest) {
 		return OAuth2Authorization.withRegisteredClient(registeredClient)
@@ -505,7 +478,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 		boolean redirectOnError = true;
 		if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
 				(parameterName.equals(OAuth2ParameterNames.CLIENT_ID) ||
-						parameterName.equals(OAuth2ParameterNames.REDIRECT_URI) ||
 						parameterName.equals(OAuth2ParameterNames.STATE))) {
 			redirectOnError = false;
 		}
@@ -520,14 +492,14 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 					.redirectUri(redirectUri)
 					.state(state)
 					.build();
-			authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
 		} else if (!redirectOnError && StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
 			authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
 					.redirectUri(null)		// Prevent redirects
 					.build();
-			authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
 		}
 
+		authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
+
 		throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult);
 	}
 
@@ -569,124 +541,4 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 
 	}
 
-	private static class DefaultRedirectUriOAuth2AuthenticationValidator implements OAuth2AuthenticationValidator {
-
-		@Override
-		public void validate(OAuth2AuthenticationContext authenticationContext) {
-			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
-					authenticationContext.getAuthentication();
-			RegisteredClient registeredClient = authenticationContext.get(RegisteredClient.class);
-
-			String requestedRedirectUri = authorizationCodeRequestAuthentication.getRedirectUri();
-
-			if (StringUtils.hasText(requestedRedirectUri)) {
-				// ***** redirect_uri is available in authorization request
-
-				UriComponents requestedRedirect = null;
-				try {
-					requestedRedirect = UriComponentsBuilder.fromUriString(requestedRedirectUri).build();
-				} catch (Exception ex) { }
-				if (requestedRedirect == null || requestedRedirect.getFragment() != null) {
-					throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
-							authorizationCodeRequestAuthentication, registeredClient);
-				}
-
-				String requestedRedirectHost = requestedRedirect.getHost();
-				if (requestedRedirectHost == null || requestedRedirectHost.equals("localhost")) {
-					// As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1
-					// While redirect URIs using localhost (i.e., "http://localhost:{port}/{path}")
-					// function similarly to loopback IP redirects described in Section 10.3.3,
-					// the use of "localhost" is NOT RECOMMENDED.
-					OAuth2Error error = new OAuth2Error(
-							OAuth2ErrorCodes.INVALID_REQUEST,
-							"localhost is not allowed for the redirect_uri (" + requestedRedirectUri + "). " +
-									"Use the IP literal (127.0.0.1) instead.",
-							"https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1");
-					throwError(error, OAuth2ParameterNames.REDIRECT_URI,
-							authorizationCodeRequestAuthentication, registeredClient, null);
-				}
-
-				if (!isLoopbackAddress(requestedRedirectHost)) {
-					// As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7
-					// When comparing client redirect URIs against pre-registered URIs,
-					// authorization servers MUST utilize exact string matching.
-					if (!registeredClient.getRedirectUris().contains(requestedRedirectUri)) {
-						throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
-								authorizationCodeRequestAuthentication, registeredClient);
-					}
-				} else {
-					// As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-10.3.3
-					// The authorization server MUST allow any port to be specified at the
-					// time of the request for loopback IP redirect URIs, to accommodate
-					// clients that obtain an available ephemeral port from the operating
-					// system at the time of the request.
-					boolean validRedirectUri = false;
-					for (String registeredRedirectUri : registeredClient.getRedirectUris()) {
-						UriComponentsBuilder registeredRedirect = UriComponentsBuilder.fromUriString(registeredRedirectUri);
-						registeredRedirect.port(requestedRedirect.getPort());
-						if (registeredRedirect.build().toString().equals(requestedRedirect.toString())) {
-							validRedirectUri = true;
-							break;
-						}
-					}
-					if (!validRedirectUri) {
-						throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
-								authorizationCodeRequestAuthentication, registeredClient);
-					}
-				}
-
-			} else {
-				// ***** redirect_uri is NOT available in authorization request
-
-				if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID) ||
-						registeredClient.getRedirectUris().size() != 1) {
-					// redirect_uri is REQUIRED for OpenID Connect
-					throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
-							authorizationCodeRequestAuthentication, registeredClient);
-				}
-			}
-		}
-
-		private static boolean isLoopbackAddress(String host) {
-			// IPv6 loopback address should either be "0:0:0:0:0:0:0:1" or "::1"
-			if ("[0:0:0:0:0:0:0:1]".equals(host) || "[::1]".equals(host)) {
-				return true;
-			}
-			// IPv4 loopback address ranges from 127.0.0.1 to 127.255.255.255
-			String[] ipv4Octets = host.split("\\.");
-			if (ipv4Octets.length != 4) {
-				return false;
-			}
-			try {
-				int[] address = new int[ipv4Octets.length];
-				for (int i=0; i < ipv4Octets.length; i++) {
-					address[i] = Integer.parseInt(ipv4Octets[i]);
-				}
-				return address[0] == 127 && address[1] >= 0 && address[1] <= 255 && address[2] >= 0 &&
-						address[2] <= 255 && address[3] >= 1 && address[3] <= 255;
-			} catch (NumberFormatException ex) {
-				return false;
-			}
-		}
-
-	}
-
-	private static class DefaultScopeOAuth2AuthenticationValidator implements OAuth2AuthenticationValidator {
-
-		@Override
-		public void validate(OAuth2AuthenticationContext authenticationContext) {
-			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
-					authenticationContext.getAuthentication();
-			RegisteredClient registeredClient = authenticationContext.get(RegisteredClient.class);
-
-			Set<String> requestedScopes = authorizationCodeRequestAuthentication.getScopes();
-			Set<String> allowedScopes = registeredClient.getScopes();
-			if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
-				throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
-						authorizationCodeRequestAuthentication, registeredClient);
-			}
-		}
-
-	}
-
 }

+ 226 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java

@@ -0,0 +1,226 @@
+/*
+ * Copyright 2020-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization.authentication;
+
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.oidc.OidcScopes;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.util.StringUtils;
+import org.springframework.web.util.UriComponents;
+import org.springframework.web.util.UriComponentsBuilder;
+
+/**
+ * A {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext}
+ * containing an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
+ * and is the default {@link OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthenticationValidator(Consumer) authentication validator}
+ * used for validating specific OAuth 2.0 Authorization Request parameters used in the Authorization Code Grant.
+ *
+ * <p>
+ * The default implementation first validates {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getRedirectUri()}
+ * and then {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getScopes()}.
+ * If validation fails, an {@link OAuth2AuthorizationCodeRequestAuthenticationException} is thrown.
+ *
+ * @author Joe Grandja
+ * @since 0.4.0
+ * @see OAuth2AuthorizationCodeRequestAuthenticationContext
+ * @see OAuth2AuthorizationCodeRequestAuthenticationToken
+ * @see OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthenticationValidator(Consumer)
+ */
+public final class OAuth2AuthorizationCodeRequestAuthenticationValidator implements Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> {
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
+
+	/**
+	 * The default validator for {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getScopes()}.
+	 */
+	public static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_SCOPE_VALIDATOR =
+			OAuth2AuthorizationCodeRequestAuthenticationValidator::validateScope;
+
+	/**
+	 * The default validator for {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getRedirectUri()}.
+	 */
+	public static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_REDIRECT_URI_VALIDATOR =
+			OAuth2AuthorizationCodeRequestAuthenticationValidator::validateRedirectUri;
+
+	private final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator =
+			DEFAULT_REDIRECT_URI_VALIDATOR.andThen(DEFAULT_SCOPE_VALIDATOR);
+
+	@Override
+	public void accept(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
+		this.authenticationValidator.accept(authenticationContext);
+	}
+
+	private static void validateScope(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
+				authenticationContext.getAuthentication();
+		RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
+
+		Set<String> requestedScopes = authorizationCodeRequestAuthentication.getScopes();
+		Set<String> allowedScopes = registeredClient.getScopes();
+		if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
+			throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
+					authorizationCodeRequestAuthentication, registeredClient);
+		}
+	}
+
+	private static void validateRedirectUri(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
+				authenticationContext.getAuthentication();
+		RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
+
+		String requestedRedirectUri = authorizationCodeRequestAuthentication.getRedirectUri();
+
+		if (StringUtils.hasText(requestedRedirectUri)) {
+			// ***** redirect_uri is available in authorization request
+
+			UriComponents requestedRedirect = null;
+			try {
+				requestedRedirect = UriComponentsBuilder.fromUriString(requestedRedirectUri).build();
+			} catch (Exception ex) { }
+			if (requestedRedirect == null || requestedRedirect.getFragment() != null) {
+				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
+						authorizationCodeRequestAuthentication, registeredClient);
+			}
+
+			String requestedRedirectHost = requestedRedirect.getHost();
+			if (requestedRedirectHost == null || requestedRedirectHost.equals("localhost")) {
+				// As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1
+				// While redirect URIs using localhost (i.e., "http://localhost:{port}/{path}")
+				// function similarly to loopback IP redirects described in Section 10.3.3,
+				// the use of "localhost" is NOT RECOMMENDED.
+				OAuth2Error error = new OAuth2Error(
+						OAuth2ErrorCodes.INVALID_REQUEST,
+						"localhost is not allowed for the redirect_uri (" + requestedRedirectUri + "). " +
+								"Use the IP literal (127.0.0.1) instead.",
+						"https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1");
+				throwError(error, OAuth2ParameterNames.REDIRECT_URI,
+						authorizationCodeRequestAuthentication, registeredClient);
+			}
+
+			if (!isLoopbackAddress(requestedRedirectHost)) {
+				// As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7
+				// When comparing client redirect URIs against pre-registered URIs,
+				// authorization servers MUST utilize exact string matching.
+				if (!registeredClient.getRedirectUris().contains(requestedRedirectUri)) {
+					throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
+							authorizationCodeRequestAuthentication, registeredClient);
+				}
+			} else {
+				// As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-10.3.3
+				// The authorization server MUST allow any port to be specified at the
+				// time of the request for loopback IP redirect URIs, to accommodate
+				// clients that obtain an available ephemeral port from the operating
+				// system at the time of the request.
+				boolean validRedirectUri = false;
+				for (String registeredRedirectUri : registeredClient.getRedirectUris()) {
+					UriComponentsBuilder registeredRedirect = UriComponentsBuilder.fromUriString(registeredRedirectUri);
+					registeredRedirect.port(requestedRedirect.getPort());
+					if (registeredRedirect.build().toString().equals(requestedRedirect.toString())) {
+						validRedirectUri = true;
+						break;
+					}
+				}
+				if (!validRedirectUri) {
+					throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
+							authorizationCodeRequestAuthentication, registeredClient);
+				}
+			}
+
+		} else {
+			// ***** redirect_uri is NOT available in authorization request
+
+			if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID) ||
+					registeredClient.getRedirectUris().size() != 1) {
+				// redirect_uri is REQUIRED for OpenID Connect
+				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
+						authorizationCodeRequestAuthentication, registeredClient);
+			}
+		}
+	}
+
+	private static boolean isLoopbackAddress(String host) {
+		// IPv6 loopback address should either be "0:0:0:0:0:0:0:1" or "::1"
+		if ("[0:0:0:0:0:0:0:1]".equals(host) || "[::1]".equals(host)) {
+			return true;
+		}
+		// IPv4 loopback address ranges from 127.0.0.1 to 127.255.255.255
+		String[] ipv4Octets = host.split("\\.");
+		if (ipv4Octets.length != 4) {
+			return false;
+		}
+		try {
+			int[] address = new int[ipv4Octets.length];
+			for (int i=0; i < ipv4Octets.length; i++) {
+				address[i] = Integer.parseInt(ipv4Octets[i]);
+			}
+			return address[0] == 127 && address[1] >= 0 && address[1] <= 255 && address[2] >= 0 &&
+					address[2] <= 255 && address[3] >= 1 && address[3] <= 255;
+		} catch (NumberFormatException ex) {
+			return false;
+		}
+	}
+
+	private static void throwError(String errorCode, String parameterName,
+			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
+			RegisteredClient registeredClient) {
+		OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, ERROR_URI);
+		throwError(error, parameterName, authorizationCodeRequestAuthentication, registeredClient);
+	}
+
+	private static void throwError(OAuth2Error error, String parameterName,
+			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
+			RegisteredClient registeredClient) {
+
+		boolean redirectOnError = true;
+		if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
+				parameterName.equals(OAuth2ParameterNames.REDIRECT_URI)) {
+			redirectOnError = false;
+		}
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = authorizationCodeRequestAuthentication;
+
+		if (redirectOnError && !StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
+			String redirectUri = registeredClient.getRedirectUris().iterator().next();
+			authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
+					.redirectUri(redirectUri)
+					.build();
+		} else if (!redirectOnError && StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
+			authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
+					.redirectUri(null)		// Prevent redirects
+					.build();
+		}
+
+		authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
+
+		throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult);
+	}
+
+	private static OAuth2AuthorizationCodeRequestAuthenticationToken.Builder from(OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
+		return OAuth2AuthorizationCodeRequestAuthenticationToken.with(authorizationCodeRequestAuthentication.getClientId(), (Authentication) authorizationCodeRequestAuthentication.getPrincipal())
+				.authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri())
+				.redirectUri(authorizationCodeRequestAuthentication.getRedirectUri())
+				.scopes(authorizationCodeRequestAuthentication.getScopes())
+				.state(authorizationCodeRequestAuthentication.getState())
+				.additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters())
+				.authorizationCode(authorizationCodeRequestAuthentication.getAuthorizationCode());
+	}
+
+}

+ 21 - 2
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContext.java

@@ -15,8 +15,12 @@
  */
 package org.springframework.security.oauth2.server.authorization.authentication;
 
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
+import java.util.function.Consumer;
 
+import org.springframework.lang.Nullable;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
@@ -32,11 +36,26 @@ import org.springframework.util.Assert;
  * @since 0.2.1
  * @see OAuth2AuthenticationContext
  * @see OAuth2AuthorizationConsent
+ * @see OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthorizationConsentCustomizer(Consumer)
  */
-public final class OAuth2AuthorizationConsentAuthenticationContext extends OAuth2AuthenticationContext {
+public final class OAuth2AuthorizationConsentAuthenticationContext implements OAuth2AuthenticationContext {
+	private final Map<Object, Object> context;
 
 	private OAuth2AuthorizationConsentAuthenticationContext(Map<Object, Object> context) {
-		super(context);
+		this.context = Collections.unmodifiableMap(new HashMap<>(context));
+	}
+
+	@SuppressWarnings("unchecked")
+	@Nullable
+	@Override
+	public <V> V get(Object key) {
+		return hasKey(key) ? (V) this.context.get(key) : null;
+	}
+
+	@Override
+	public boolean hasKey(Object key) {
+		Assert.notNull(key, "key cannot be null");
+		return this.context.containsKey(key);
 	}
 
 	/**

+ 62 - 11
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 jakarta.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<AuthenticationConverter> authorizationRequestConverters = new ArrayList<>();
+	private Consumer<List<AuthenticationConverter>> authorizationRequestConvertersConsumer = (authorizationRequestConverters) -> {};
 	private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+	private Consumer<List<AuthenticationProvider>> 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<List<AuthenticationConverter>> 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<List<AuthenticationProvider>> 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<AuthenticationProvider> authenticationProviders =
-				!this.authenticationProviders.isEmpty() ?
-						this.authenticationProviders :
-						createDefaultAuthenticationProviders(httpSecurity);
+		List<AuthenticationProvider> 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<AuthenticationConverter> 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<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
+	private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
+		List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
+
+		authenticationConverters.add(new OAuth2AuthorizationCodeRequestAuthenticationConverter());
+
+		return authenticationConverters;
+	}
+
+	private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
 		List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
 
 		OAuth2AuthorizationCodeRequestAuthenticationProvider authorizationCodeRequestAuthenticationProvider =

+ 25 - 23
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java

@@ -34,7 +34,6 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
 import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter;
-import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter;
 import org.springframework.security.web.authentication.HttpStatusEntryPoint;
 import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
 import org.springframework.security.web.context.SecurityContextHolderFilter;
@@ -54,6 +53,7 @@ import org.springframework.util.Assert;
  * @since 0.0.1
  * @see AbstractHttpConfigurer
  * @see OAuth2ClientAuthenticationConfigurer
+ * @see OAuth2AuthorizationServerMetadataEndpointConfigurer
  * @see OAuth2AuthorizationEndpointConfigurer
  * @see OAuth2TokenEndpointConfigurer
  * @see OAuth2TokenIntrospectionEndpointConfigurer
@@ -63,22 +63,20 @@ import org.springframework.util.Assert;
  * @see OAuth2AuthorizationService
  * @see OAuth2AuthorizationConsentService
  * @see NimbusJwkSetEndpointFilter
- * @see OAuth2AuthorizationServerMetadataEndpointFilter
  */
 public final class OAuth2AuthorizationServerConfigurer
 		extends AbstractHttpConfigurer<OAuth2AuthorizationServerConfigurer, HttpSecurity> {
 
 	private final Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = createConfigurers();
-	private RequestMatcher jwkSetEndpointMatcher;
-	private RequestMatcher authorizationServerMetadataEndpointMatcher;
 	private final RequestMatcher endpointsMatcher = (request) ->
-			getRequestMatcher(OAuth2AuthorizationEndpointConfigurer.class).matches(request) ||
-			getRequestMatcher(OAuth2TokenEndpointConfigurer.class).matches(request) ||
-			getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class).matches(request) ||
-			getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class).matches(request) ||
-			getRequestMatcher(OidcConfigurer.class).matches(request) ||
-			this.jwkSetEndpointMatcher.matches(request) ||
-			this.authorizationServerMetadataEndpointMatcher.matches(request);
+					getRequestMatcher(OAuth2AuthorizationServerMetadataEndpointConfigurer.class).matches(request) ||
+					getRequestMatcher(OAuth2AuthorizationEndpointConfigurer.class).matches(request) ||
+					getRequestMatcher(OAuth2TokenEndpointConfigurer.class).matches(request) ||
+					getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class).matches(request) ||
+					getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class).matches(request) ||
+					getRequestMatcher(OidcConfigurer.class).matches(request) ||
+					this.jwkSetEndpointMatcher.matches(request);
+	private RequestMatcher jwkSetEndpointMatcher;
 
 	/**
 	 * Sets the repository of registered clients.
@@ -152,6 +150,18 @@ public final class OAuth2AuthorizationServerConfigurer
 		return this;
 	}
 
+	/**
+	 * Configures the OAuth 2.0 Authorization Server Metadata Endpoint.
+	 *
+	 * @param authorizationServerMetadataEndpointCustomizer the {@link Customizer} providing access to the {@link OAuth2AuthorizationServerMetadataEndpointConfigurer}
+	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
+	 * @since 0.4.0
+	 */
+	public OAuth2AuthorizationServerConfigurer authorizationServerMetadataEndpoint(Customizer<OAuth2AuthorizationServerMetadataEndpointConfigurer> authorizationServerMetadataEndpointCustomizer) {
+		authorizationServerMetadataEndpointCustomizer.customize(getConfigurer(OAuth2AuthorizationServerMetadataEndpointConfigurer.class));
+		return this;
+	}
+
 	/**
 	 * Configures the OAuth 2.0 Authorization Endpoint.
 	 *
@@ -222,7 +232,9 @@ public final class OAuth2AuthorizationServerConfigurer
 	public void init(HttpSecurity httpSecurity) {
 		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
 		validateAuthorizationServerSettings(authorizationServerSettings);
-		initEndpointMatchers(authorizationServerSettings);
+
+		this.jwkSetEndpointMatcher = new AntPathRequestMatcher(
+				authorizationServerSettings.getJwkSetEndpoint(), HttpMethod.GET.name());
 
 		this.configurers.values().forEach(configurer -> configurer.init(httpSecurity));
 
@@ -253,15 +265,12 @@ public final class OAuth2AuthorizationServerConfigurer
 					jwkSource, authorizationServerSettings.getJwkSetEndpoint());
 			httpSecurity.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
 		}
-
-		OAuth2AuthorizationServerMetadataEndpointFilter authorizationServerMetadataEndpointFilter =
-				new OAuth2AuthorizationServerMetadataEndpointFilter();
-		httpSecurity.addFilterBefore(postProcess(authorizationServerMetadataEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
 	}
 
 	private Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> createConfigurers() {
 		Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = new LinkedHashMap<>();
 		configurers.put(OAuth2ClientAuthenticationConfigurer.class, new OAuth2ClientAuthenticationConfigurer(this::postProcess));
+		configurers.put(OAuth2AuthorizationServerMetadataEndpointConfigurer.class, new OAuth2AuthorizationServerMetadataEndpointConfigurer(this::postProcess));
 		configurers.put(OAuth2AuthorizationEndpointConfigurer.class, new OAuth2AuthorizationEndpointConfigurer(this::postProcess));
 		configurers.put(OAuth2TokenEndpointConfigurer.class, new OAuth2TokenEndpointConfigurer(this::postProcess));
 		configurers.put(OAuth2TokenIntrospectionEndpointConfigurer.class, new OAuth2TokenIntrospectionEndpointConfigurer(this::postProcess));
@@ -279,13 +288,6 @@ public final class OAuth2AuthorizationServerConfigurer
 		return getConfigurer(configurerType).getRequestMatcher();
 	}
 
-	private void initEndpointMatchers(AuthorizationServerSettings authorizationServerSettings) {
-		this.jwkSetEndpointMatcher = new AntPathRequestMatcher(
-				authorizationServerSettings.getJwkSetEndpoint(), HttpMethod.GET.name());
-		this.authorizationServerMetadataEndpointMatcher = new AntPathRequestMatcher(
-				"/.well-known/oauth-authorization-server", HttpMethod.GET.name());
-	}
-
 	private static void validateAuthorizationServerSettings(AuthorizationServerSettings authorizationServerSettings) {
 		if (authorizationServerSettings.getIssuer() != null) {
 			URI issuerUri;

+ 108 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataEndpointConfigurer.java

@@ -0,0 +1,108 @@
+/*
+ * Copyright 2020-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
+
+import java.util.function.Consumer;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.config.annotation.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata;
+import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter;
+import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+
+/**
+ * Configurer for the OAuth 2.0 Authorization Server Metadata Endpoint.
+ *
+ * @author Joe Grandja
+ * @since 0.4.0
+ * @see OAuth2AuthorizationServerConfigurer#authorizationServerMetadataEndpoint
+ * @see OAuth2AuthorizationServerMetadataEndpointFilter
+ */
+public final class OAuth2AuthorizationServerMetadataEndpointConfigurer extends AbstractOAuth2Configurer {
+	private RequestMatcher requestMatcher;
+	private Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer;
+	private Consumer<OAuth2AuthorizationServerMetadata.Builder> defaultAuthorizationServerMetadataCustomizer;
+
+	/**
+	 * Restrict for internal use only.
+	 */
+	OAuth2AuthorizationServerMetadataEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
+		super(objectPostProcessor);
+	}
+
+	/**
+     * Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationServerMetadata.Builder}
+	 * allowing the ability to customize the claims of the Authorization Server's configuration.
+	 *
+	 * @param authorizationServerMetadataCustomizer the {@code Consumer} providing access to the {@link OAuth2AuthorizationServerMetadata.Builder}
+     * @return the {@link OAuth2AuthorizationServerMetadataEndpointConfigurer} for further configuration
+	 */
+	public OAuth2AuthorizationServerMetadataEndpointConfigurer authorizationServerMetadataCustomizer(
+			Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer) {
+		this.authorizationServerMetadataCustomizer = authorizationServerMetadataCustomizer;
+		return this;
+	}
+
+	void addDefaultAuthorizationServerMetadataCustomizer(
+			Consumer<OAuth2AuthorizationServerMetadata.Builder> defaultAuthorizationServerMetadataCustomizer) {
+		this.defaultAuthorizationServerMetadataCustomizer =
+				this.defaultAuthorizationServerMetadataCustomizer == null ?
+						defaultAuthorizationServerMetadataCustomizer :
+						this.defaultAuthorizationServerMetadataCustomizer.andThen(defaultAuthorizationServerMetadataCustomizer);
+	}
+
+	@Override
+	void init(HttpSecurity httpSecurity) {
+		this.requestMatcher = new AntPathRequestMatcher(
+				"/.well-known/oauth-authorization-server", HttpMethod.GET.name());
+	}
+
+	@Override
+	void configure(HttpSecurity httpSecurity) {
+		OAuth2AuthorizationServerMetadataEndpointFilter authorizationServerMetadataEndpointFilter =
+				new OAuth2AuthorizationServerMetadataEndpointFilter();
+		Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer = getAuthorizationServerMetadataCustomizer();
+		if (authorizationServerMetadataCustomizer != null) {
+			authorizationServerMetadataEndpointFilter.setAuthorizationServerMetadataCustomizer(authorizationServerMetadataCustomizer);
+		}
+		httpSecurity.addFilterBefore(postProcess(authorizationServerMetadataEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
+	}
+
+	private Consumer<OAuth2AuthorizationServerMetadata.Builder> getAuthorizationServerMetadataCustomizer() {
+		Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer = null;
+		if (this.defaultAuthorizationServerMetadataCustomizer != null || this.authorizationServerMetadataCustomizer != null) {
+			if (this.defaultAuthorizationServerMetadataCustomizer != null) {
+				authorizationServerMetadataCustomizer = this.defaultAuthorizationServerMetadataCustomizer;
+			}
+			if (this.authorizationServerMetadataCustomizer != null) {
+				authorizationServerMetadataCustomizer =
+						authorizationServerMetadataCustomizer == null ?
+								this.authorizationServerMetadataCustomizer :
+								authorizationServerMetadataCustomizer.andThen(this.authorizationServerMetadataCustomizer);
+			}
+		}
+		return authorizationServerMetadataCustomizer;
+	}
+
+	@Override
+	RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+}

+ 68 - 11
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 jakarta.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<AuthenticationConverter> authenticationConverters = new ArrayList<>();
+	private Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer = (authenticationConverters) -> {};
 	private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+	private Consumer<List<AuthenticationProvider>> 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<List<AuthenticationConverter>> 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<List<AuthenticationProvider>> 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<AuthenticationProvider> authenticationProviders =
-				!this.authenticationProviders.isEmpty() ?
-						this.authenticationProviders :
-						createDefaultAuthenticationProviders(httpSecurity);
+		List<AuthenticationProvider> 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<AuthenticationConverter> 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<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
+	private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
+		List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
+
+		authenticationConverters.add(new JwtClientAssertionAuthenticationConverter());
+		authenticationConverters.add(new ClientSecretBasicAuthenticationConverter());
+		authenticationConverters.add(new ClientSecretPostAuthenticationConverter());
+		authenticationConverters.add(new PublicClientAuthenticationConverter());
+
+		return authenticationConverters;
+	}
+
+	private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
 		List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
 
 		RegisteredClientRepository registeredClientRepository = OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity);

+ 67 - 13
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 jakarta.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<AuthenticationProvider> authenticationProviders = new LinkedList<>();
+	private final List<AuthenticationConverter> accessTokenRequestConverters = new ArrayList<>();
+	private Consumer<List<AuthenticationConverter>> accessTokenRequestConvertersConsumer = (accessTokenRequestConverters) -> {};
+	private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+	private Consumer<List<AuthenticationProvider>> 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<List<AuthenticationConverter>> 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<List<AuthenticationProvider>> 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<AuthenticationProvider> authenticationProviders =
-				!this.authenticationProviders.isEmpty() ?
-						this.authenticationProviders :
-						createDefaultAuthenticationProviders(httpSecurity);
+		List<AuthenticationProvider> 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<AuthenticationConverter> 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<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
+	private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
+		List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
+
+		authenticationConverters.add(new OAuth2AuthorizationCodeAuthenticationConverter());
+		authenticationConverters.add(new OAuth2RefreshTokenAuthenticationConverter());
+		authenticationConverters.add(new OAuth2ClientCredentialsAuthenticationConverter());
+
+		return authenticationConverters;
+	}
+
+	private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
 		List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
 
 		OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity);

+ 64 - 13
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 jakarta.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<AuthenticationProvider> authenticationProviders = new LinkedList<>();
+	private final List<AuthenticationConverter> introspectionRequestConverters = new ArrayList<>();
+	private Consumer<List<AuthenticationConverter>> introspectionRequestConvertersConsumer = (introspectionRequestConverters) -> {};
+	private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+	private Consumer<List<AuthenticationProvider>> 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<List<AuthenticationConverter>> 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<List<AuthenticationProvider>> 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<AuthenticationProvider> authenticationProviders =
-				!this.authenticationProviders.isEmpty() ?
-						this.authenticationProviders :
-						createDefaultAuthenticationProviders(httpSecurity);
+		List<AuthenticationProvider> 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<AuthenticationConverter> 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<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
+	private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
+		List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
+
+		authenticationConverters.add(new OAuth2TokenIntrospectionAuthenticationConverter());
+
+		return authenticationConverters;
+	}
+
+	private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
 		List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
 
 		OAuth2TokenIntrospectionAuthenticationProvider tokenIntrospectionAuthenticationProvider =

+ 65 - 14
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 jakarta.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<AuthenticationProvider> authenticationProviders = new LinkedList<>();
+	private final List<AuthenticationConverter> revocationRequestConverters = new ArrayList<>();
+	private Consumer<List<AuthenticationConverter>> revocationRequestConvertersConsumer = (revocationRequestConverters) -> {};
+	private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+	private Consumer<List<AuthenticationProvider>> 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<List<AuthenticationConverter>> 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<List<AuthenticationProvider>> 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<AuthenticationProvider> authenticationProviders =
-				!this.authenticationProviders.isEmpty() ?
-						this.authenticationProviders :
-						createDefaultAuthenticationProviders(httpSecurity);
+		List<AuthenticationProvider> 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<AuthenticationConverter> 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<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
+	private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
+		List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
+
+		authenticationConverters.add(new OAuth2TokenRevocationAuthenticationConverter());
+
+		return authenticationConverters;
+	}
+
+	private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
 		List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
 
 		OAuth2TokenRevocationAuthenticationProvider tokenRevocationAuthenticationProvider =

+ 19 - 32
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java

@@ -20,13 +20,9 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
-import org.springframework.http.HttpMethod;
 import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.ObjectPostProcessor;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
-import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
-import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.OrRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 
@@ -36,9 +32,9 @@ import org.springframework.security.web.util.matcher.RequestMatcher;
  * @author Joe Grandja
  * @since 0.2.0
  * @see OAuth2AuthorizationServerConfigurer#oidc
+ * @see OidcProviderConfigurationEndpointConfigurer
  * @see OidcClientRegistrationEndpointConfigurer
  * @see OidcUserInfoEndpointConfigurer
- * @see OidcProviderConfigurationEndpointFilter
  */
 public final class OidcConfigurer extends AbstractOAuth2Configurer {
 	private final Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = new LinkedHashMap<>();
@@ -49,9 +45,22 @@ public final class OidcConfigurer extends AbstractOAuth2Configurer {
 	 */
 	OidcConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
 		super(objectPostProcessor);
+		addConfigurer(OidcProviderConfigurationEndpointConfigurer.class, new OidcProviderConfigurationEndpointConfigurer(objectPostProcessor));
 		addConfigurer(OidcUserInfoEndpointConfigurer.class, new OidcUserInfoEndpointConfigurer(objectPostProcessor));
 	}
 
+	/**
+	 * Configures the OpenID Connect 1.0 Provider Configuration Endpoint.
+	 *
+	 * @param providerConfigurationEndpointCustomizer the {@link Customizer} providing access to the {@link OidcProviderConfigurationEndpointConfigurer}
+	 * @return the {@link OidcConfigurer} for further configuration
+	 * @since 0.4.0
+	 */
+	public OidcConfigurer providerConfigurationEndpoint(Customizer<OidcProviderConfigurationEndpointConfigurer> providerConfigurationEndpointCustomizer) {
+		providerConfigurationEndpointCustomizer.customize(getConfigurer(OidcProviderConfigurationEndpointConfigurer.class));
+		return this;
+	}
+
 	/**
 	 * Configures the OpenID Connect Dynamic Client Registration 1.0 Endpoint.
 	 *
@@ -83,39 +92,17 @@ public final class OidcConfigurer extends AbstractOAuth2Configurer {
 
 	@Override
 	void init(HttpSecurity httpSecurity) {
-		OidcUserInfoEndpointConfigurer userInfoEndpointConfigurer =
-				getConfigurer(OidcUserInfoEndpointConfigurer.class);
-		userInfoEndpointConfigurer.init(httpSecurity);
-		OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer =
-				getConfigurer(OidcClientRegistrationEndpointConfigurer.class);
-		if (clientRegistrationEndpointConfigurer != null) {
-			clientRegistrationEndpointConfigurer.init(httpSecurity);
-		}
-
 		List<RequestMatcher> requestMatchers = new ArrayList<>();
-		requestMatchers.add(new AntPathRequestMatcher(
-				"/.well-known/openid-configuration", HttpMethod.GET.name()));
-		requestMatchers.add(userInfoEndpointConfigurer.getRequestMatcher());
-		if (clientRegistrationEndpointConfigurer != null) {
-			requestMatchers.add(clientRegistrationEndpointConfigurer.getRequestMatcher());
-		}
+		this.configurers.values().forEach(configurer -> {
+			configurer.init(httpSecurity);
+			requestMatchers.add(configurer.getRequestMatcher());
+		});
 		this.requestMatcher = new OrRequestMatcher(requestMatchers);
 	}
 
 	@Override
 	void configure(HttpSecurity httpSecurity) {
-		OidcUserInfoEndpointConfigurer userInfoEndpointConfigurer =
-				getConfigurer(OidcUserInfoEndpointConfigurer.class);
-		userInfoEndpointConfigurer.configure(httpSecurity);
-		OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer =
-				getConfigurer(OidcClientRegistrationEndpointConfigurer.class);
-		if (clientRegistrationEndpointConfigurer != null) {
-			clientRegistrationEndpointConfigurer.configure(httpSecurity);
-		}
-
-		OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter =
-				new OidcProviderConfigurationEndpointFilter();
-		httpSecurity.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
+		this.configurers.values().forEach(configurer -> configurer.configure(httpSecurity));
 	}
 
 	@Override

+ 108 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationEndpointConfigurer.java

@@ -0,0 +1,108 @@
+/*
+ * Copyright 2020-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
+
+import java.util.function.Consumer;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.config.annotation.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.oauth2.server.authorization.oidc.OidcProviderConfiguration;
+import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
+import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+
+/**
+ * Configurer for the OpenID Connect 1.0 Provider Configuration Endpoint.
+ *
+ * @author Joe Grandja
+ * @since 0.4.0
+ * @see OidcConfigurer#providerConfigurationEndpoint
+ * @see OidcProviderConfigurationEndpointFilter
+ */
+public final class OidcProviderConfigurationEndpointConfigurer extends AbstractOAuth2Configurer {
+	private RequestMatcher requestMatcher;
+	private Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer;
+	private Consumer<OidcProviderConfiguration.Builder> defaultProviderConfigurationCustomizer;
+
+	/**
+	 * Restrict for internal use only.
+	 */
+	OidcProviderConfigurationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
+		super(objectPostProcessor);
+	}
+
+	/**
+	 * Sets the {@code Consumer} providing access to the {@link OidcProviderConfiguration.Builder}
+	 * allowing the ability to customize the claims of the OpenID Provider's configuration.
+	 *
+	 * @param providerConfigurationCustomizer the {@code Consumer} providing access to the {@link OidcProviderConfiguration.Builder}
+	 * @return the {@link OidcProviderConfigurationEndpointConfigurer} for further configuration
+	 */
+	public OidcProviderConfigurationEndpointConfigurer providerConfigurationCustomizer(
+			Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer) {
+		this.providerConfigurationCustomizer = providerConfigurationCustomizer;
+		return this;
+	}
+
+	void addDefaultProviderConfigurationCustomizer(
+			Consumer<OidcProviderConfiguration.Builder> defaultProviderConfigurationCustomizer) {
+		this.defaultProviderConfigurationCustomizer =
+				this.defaultProviderConfigurationCustomizer == null ?
+						defaultProviderConfigurationCustomizer :
+						this.defaultProviderConfigurationCustomizer.andThen(defaultProviderConfigurationCustomizer);
+	}
+
+	@Override
+	void init(HttpSecurity httpSecurity) {
+		this.requestMatcher = new AntPathRequestMatcher(
+				"/.well-known/openid-configuration", HttpMethod.GET.name());
+	}
+
+	@Override
+	void configure(HttpSecurity httpSecurity) {
+		OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter =
+				new OidcProviderConfigurationEndpointFilter();
+		Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer = getProviderConfigurationCustomizer();
+		if (providerConfigurationCustomizer != null) {
+			oidcProviderConfigurationEndpointFilter.setProviderConfigurationCustomizer(providerConfigurationCustomizer);
+		}
+		httpSecurity.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
+	}
+
+	private Consumer<OidcProviderConfiguration.Builder> getProviderConfigurationCustomizer() {
+		Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer = null;
+		if (this.defaultProviderConfigurationCustomizer != null || this.providerConfigurationCustomizer != null) {
+			if (this.defaultProviderConfigurationCustomizer != null) {
+				providerConfigurationCustomizer = this.defaultProviderConfigurationCustomizer;
+			}
+			if (this.providerConfigurationCustomizer != null) {
+				providerConfigurationCustomizer =
+						providerConfigurationCustomizer == null ?
+								this.providerConfigurationCustomizer :
+								providerConfigurationCustomizer.andThen(this.providerConfigurationCustomizer);
+			}
+		}
+		return providerConfigurationCustomizer;
+	}
+
+	@Override
+	RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+}

+ 20 - 2
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcUserInfoAuthenticationContext.java

@@ -15,9 +15,12 @@
  */
 package org.springframework.security.oauth2.server.authorization.oidc.authentication;
 
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.function.Function;
 
+import org.springframework.lang.Nullable;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
@@ -31,12 +34,27 @@ import org.springframework.util.Assert;
  * @author Joe Grandja
  * @since 0.2.1
  * @see OAuth2AuthenticationContext
+ * @see OidcUserInfo
  * @see OidcUserInfoAuthenticationProvider#setUserInfoMapper(Function)
  */
-public final class OidcUserInfoAuthenticationContext extends OAuth2AuthenticationContext {
+public final class OidcUserInfoAuthenticationContext implements OAuth2AuthenticationContext {
+	private final Map<Object, Object> context;
 
 	private OidcUserInfoAuthenticationContext(Map<Object, Object> context) {
-		super(context);
+		this.context = Collections.unmodifiableMap(new HashMap<>(context));
+	}
+
+	@SuppressWarnings("unchecked")
+	@Nullable
+	@Override
+	public <V> V get(Object key) {
+		return hasKey(key) ? (V) this.context.get(key) : null;
+	}
+
+	@Override
+	public boolean hasKey(Object key) {
+		Assert.notNull(key, "key cannot be null");
+		return this.context.containsKey(key);
 	}
 
 	/**

+ 20 - 4
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java

@@ -39,6 +39,7 @@ import org.springframework.security.oauth2.server.authorization.oidc.http.conver
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
 import org.springframework.web.filter.OncePerRequestFilter;
 import org.springframework.web.util.UriComponentsBuilder;
 
@@ -46,6 +47,7 @@ import org.springframework.web.util.UriComponentsBuilder;
  * A {@code Filter} that processes OpenID Provider Configuration Requests.
  *
  * @author Daniel Garnier-Moiroux
+ * @author Joe Grandja
  * @since 0.1.0
  * @see OidcProviderConfiguration
  * @see AuthorizationServerSettings
@@ -62,6 +64,19 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques
 			HttpMethod.GET.name());
 	private final OidcProviderConfigurationHttpMessageConverter providerConfigurationHttpMessageConverter =
 			new OidcProviderConfigurationHttpMessageConverter();
+	private Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer = (providerConfiguration) -> {};
+
+	/**
+	 * Sets the {@code Consumer} providing access to the {@link OidcProviderConfiguration.Builder}
+	 * allowing the ability to customize the claims of the OpenID Provider's configuration.
+	 *
+	 * @param providerConfigurationCustomizer the {@code Consumer} providing access to the {@link OidcProviderConfiguration.Builder}
+	 * @since 0.4.0
+	 */
+	public void setProviderConfigurationCustomizer(Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer) {
+		Assert.notNull(providerConfigurationCustomizer, "providerConfigurationCustomizer cannot be null");
+		this.providerConfigurationCustomizer = providerConfigurationCustomizer;
+	}
 
 	@Override
 	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
@@ -76,7 +91,7 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques
 		String issuer = authorizationServerContext.getIssuer();
 		AuthorizationServerSettings authorizationServerSettings = authorizationServerContext.getAuthorizationServerSettings();
 
-		OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.builder()
+		OidcProviderConfiguration.Builder providerConfiguration = OidcProviderConfiguration.builder()
 				.issuer(issuer)
 				.authorizationEndpoint(asUrl(issuer, authorizationServerSettings.getAuthorizationEndpoint()))
 				.tokenEndpoint(asUrl(issuer, authorizationServerSettings.getTokenEndpoint()))
@@ -93,12 +108,13 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques
 				.tokenIntrospectionEndpointAuthenticationMethods(clientAuthenticationMethods())
 				.subjectType("public")
 				.idTokenSigningAlgorithm(SignatureAlgorithm.RS256.getName())
-				.scope(OidcScopes.OPENID)
-				.build();
+				.scope(OidcScopes.OPENID);
+
+		this.providerConfigurationCustomizer.accept(providerConfiguration);
 
 		ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
 		this.providerConfigurationHttpMessageConverter.write(
-				providerConfiguration, MediaType.APPLICATION_JSON, httpResponse);
+				providerConfiguration.build(), MediaType.APPLICATION_JSON, httpResponse);
 	}
 
 	private static Consumer<List<String>> clientAuthenticationMethods() {

+ 6 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java

@@ -27,6 +27,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
 import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
 import org.springframework.security.oauth2.core.oidc.OidcIdToken;
 import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
+import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
 import org.springframework.security.oauth2.jwt.JwsHeader;
 import org.springframework.security.oauth2.jwt.Jwt;
@@ -89,9 +90,13 @@ public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
 
 		Instant issuedAt = Instant.now();
 		Instant expiresAt;
+		JwsAlgorithm jwsAlgorithm = SignatureAlgorithm.RS256;
 		if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) {
 			// TODO Allow configuration for ID Token time-to-live
 			expiresAt = issuedAt.plus(30, ChronoUnit.MINUTES);
+			if (registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm() != null) {
+				jwsAlgorithm = registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm();
+			}
 		} else {
 			expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());
 		}
@@ -125,7 +130,7 @@ public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
 		}
 		// @formatter:on
 
-		JwsHeader.Builder jwsHeaderBuilder = JwsHeader.with(SignatureAlgorithm.RS256);
+		JwsHeader.Builder jwsHeaderBuilder = JwsHeader.with(jwsAlgorithm);
 
 		if (this.jwtCustomizer != null) {
 			// @formatter:off

+ 20 - 4
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java

@@ -37,6 +37,7 @@ import org.springframework.security.oauth2.server.authorization.http.converter.O
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
 import org.springframework.web.filter.OncePerRequestFilter;
 import org.springframework.web.util.UriComponentsBuilder;
 
@@ -44,6 +45,7 @@ import org.springframework.web.util.UriComponentsBuilder;
  * A {@code Filter} that processes OAuth 2.0 Authorization Server Metadata Requests.
  *
  * @author Daniel Garnier-Moiroux
+ * @author Joe Grandja
  * @since 0.1.1
  * @see OAuth2AuthorizationServerMetadata
  * @see AuthorizationServerSettings
@@ -60,6 +62,19 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP
 			HttpMethod.GET.name());
 	private final OAuth2AuthorizationServerMetadataHttpMessageConverter authorizationServerMetadataHttpMessageConverter =
 			new OAuth2AuthorizationServerMetadataHttpMessageConverter();
+	private Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer = (authorizationServerMetadata) -> {};
+
+	/**
+	 * Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationServerMetadata.Builder}
+	 * allowing the ability to customize the claims of the Authorization Server's configuration.
+	 *
+	 * @param authorizationServerMetadataCustomizer the {@code Consumer} providing access to the {@link OAuth2AuthorizationServerMetadata.Builder}
+	 * @since 0.4.0
+	 */
+	public void setAuthorizationServerMetadataCustomizer(Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer) {
+		Assert.notNull(authorizationServerMetadataCustomizer, "authorizationServerMetadataCustomizer cannot be null");
+		this.authorizationServerMetadataCustomizer = authorizationServerMetadataCustomizer;
+	}
 
 	@Override
 	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
@@ -74,7 +89,7 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP
 		String issuer = authorizationServerContext.getIssuer();
 		AuthorizationServerSettings authorizationServerSettings = authorizationServerContext.getAuthorizationServerSettings();
 
-		OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.builder()
+		OAuth2AuthorizationServerMetadata.Builder authorizationServerMetadata = OAuth2AuthorizationServerMetadata.builder()
 				.issuer(issuer)
 				.authorizationEndpoint(asUrl(issuer, authorizationServerSettings.getAuthorizationEndpoint()))
 				.tokenEndpoint(asUrl(issuer, authorizationServerSettings.getTokenEndpoint()))
@@ -88,12 +103,13 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP
 				.tokenRevocationEndpointAuthenticationMethods(clientAuthenticationMethods())
 				.tokenIntrospectionEndpoint(asUrl(issuer, authorizationServerSettings.getTokenIntrospectionEndpoint()))
 				.tokenIntrospectionEndpointAuthenticationMethods(clientAuthenticationMethods())
-				.codeChallengeMethod("S256")
-				.build();
+				.codeChallengeMethod("S256");
+
+		this.authorizationServerMetadataCustomizer.accept(authorizationServerMetadata);
 
 		ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
 		this.authorizationServerMetadataHttpMessageConverter.write(
-				authorizationServerMetadata, MediaType.APPLICATION_JSON, httpResponse);
+				authorizationServerMetadata.build(), MediaType.APPLICATION_JSON, httpResponse);
 	}
 
 	private static Consumer<List<String>> clientAuthenticationMethods() {

+ 3 - 51
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 jakarta.servlet.FilterChain;
 import jakarta.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<OAuth2TokenIntrospection> tokenIntrospectionHttpResponseConverter =
 			new OAuth2TokenIntrospectionHttpMessageConverter();
 	private final HttpMessageConverter<OAuth2Error> 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<String, String> 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<String, Object> 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);
-		}
-
-	}
-
 }

+ 5 - 40
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<OAuth2Error> 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<String, String> 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);
-		}
-	}
 }

+ 86 - 0
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<String, String> 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<String, Object> 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);
+	}
+
+}

+ 74 - 0
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<String, String> 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);
+	}
+
+}

+ 22 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/ClientSecretAuthenticationProviderTests.java

@@ -15,6 +15,8 @@
  */
 package org.springframework.security.oauth2.server.authorization.authentication;
 
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -182,6 +184,26 @@ public class ClientSecretAuthenticationProviderTests {
 		verify(this.passwordEncoder).matches(any(), any());
 	}
 
+	@Test
+	public void authenticateWhenExpiredClientSecretThenThrowOAuth2AuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.clientSecretExpiresAt(Instant.now().minus(1, ChronoUnit.HOURS).truncatedTo(ChronoUnit.SECONDS))
+				.build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
+				registeredClient.getClientId(), ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret(), null);
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
+				.satisfies(error -> {
+					assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
+					assertThat(error.getDescription()).contains("client_secret_expires_at");
+				});
+		verify(this.passwordEncoder).matches(any(), any());
+	}
+
 	@Test
 	public void authenticateWhenValidCredentialsThenAuthenticated() {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();

+ 7 - 12
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java

@@ -22,7 +22,6 @@ import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
-import java.util.function.Function;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -60,7 +59,6 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -128,10 +126,10 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 	}
 
 	@Test
-	public void setAuthenticationValidatorResolverWhenNullThenThrowIllegalArgumentException() {
-		assertThatThrownBy(() -> this.authenticationProvider.setAuthenticationValidatorResolver(null))
+	public void setAuthenticationValidatorWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.authenticationProvider.setAuthenticationValidator(null))
 				.isInstanceOf(IllegalArgumentException.class)
-				.hasMessage("authenticationValidatorResolver cannot be null");
+				.hasMessage("authenticationValidator cannot be null");
 	}
 
 	@Test
@@ -555,14 +553,14 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 	}
 
 	@Test
-	public void authenticateWhenCustomAuthenticationValidatorResolverThenUsed() {
+	public void authenticateWhenCustomAuthenticationValidatorThenUsed() {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
 		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
 				.thenReturn(registeredClient);
 
 		@SuppressWarnings("unchecked")
-		Function<String, OAuth2AuthenticationValidator> authenticationValidatorResolver = mock(Function.class);
-		this.authenticationProvider.setAuthenticationValidatorResolver(authenticationValidatorResolver);
+		Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator = mock(Consumer.class);
+		this.authenticationProvider.setAuthenticationValidator(authenticationValidator);
 
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
 				authorizationCodeRequestAuthentication(registeredClient, this.principal)
@@ -573,10 +571,7 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 
 		assertAuthorizationCodeRequestWithAuthorizationCodeResult(registeredClient, authentication, authenticationResult);
 
-		ArgumentCaptor<String> parameterNameCaptor = ArgumentCaptor.forClass(String.class);
-		verify(authenticationValidatorResolver, times(2)).apply(parameterNameCaptor.capture());
-		assertThat(parameterNameCaptor.getAllValues()).containsExactly(
-				OAuth2ParameterNames.REDIRECT_URI, OAuth2ParameterNames.SCOPE);
+		verify(authenticationValidator).accept(any());
 	}
 
 	private void assertAuthorizationCodeRequestWithAuthorizationCodeResult(

+ 25 - 0
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<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
 			new OAuth2AccessTokenResponseHttpMessageConverter();
 	private static AuthenticationConverter authorizationRequestConverter;
+	private static Consumer<List<AuthenticationConverter>> authorizationRequestConvertersConsumer;
 	private static AuthenticationProvider authorizationRequestAuthenticationProvider;
+	private static Consumer<List<AuthenticationProvider>> 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<List<AuthenticationConverter>> authenticationConvertersCaptor = ArgumentCaptor.forClass(List.class);
+		verify(authorizationRequestConvertersConsumer).accept(authenticationConvertersCaptor.capture());
+		List<AuthenticationConverter> authenticationConverters = authenticationConvertersCaptor.getValue();
+		assertThat(authenticationConverters).allMatch((converter) ->
+				converter == authorizationRequestConverter ||
+						converter instanceof OAuth2AuthorizationCodeRequestAuthenticationConverter);
+
 		verify(authorizationRequestAuthenticationProvider).authenticate(eq(authorizationCodeRequestAuthenticationResult));
+
+		@SuppressWarnings("unchecked")
+		ArgumentCaptor<List<AuthenticationProvider>> authenticationProvidersCaptor = ArgumentCaptor.forClass(List.class);
+		verify(authorizationRequestAuthenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture());
+		List<AuthenticationProvider> authenticationProviders = authenticationProvidersCaptor.getValue();
+		assertThat(authenticationProviders).allMatch((provider) ->
+				provider == authorizationRequestAuthenticationProvider ||
+						provider instanceof OAuth2AuthorizationCodeRequestAuthenticationProvider);
+
 		verify(authorizationResponseHandler).onAuthenticationSuccess(any(), any(), eq(authorizationCodeRequestAuthenticationResult));
 	}
 
@@ -998,7 +1021,9 @@ public class OAuth2AuthorizationCodeGrantTests {
 					.authorizationEndpoint(authorizationEndpoint ->
 							authorizationEndpoint
 									.authorizationRequestConverter(authorizationRequestConverter)
+									.authorizationRequestConverters(authorizationRequestConvertersConsumer)
 									.authenticationProvider(authorizationRequestAuthenticationProvider)
+									.authenticationProviders(authorizationRequestAuthenticationProvidersConsumer)
 									.authorizationResponseHandler(authorizationResponseHandler)
 									.errorResponseHandler(authorizationErrorResponseHandler));
 			RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();

+ 56 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataTests.java

@@ -15,6 +15,8 @@
  */
 package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
 
+import java.util.function.Consumer;
+
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jose.jwk.source.JWKSource;
 import com.nimbusds.jose.proc.SecurityContext;
@@ -26,14 +28,18 @@ import org.junit.Test;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
 import org.springframework.jdbc.core.JdbcOperations;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
 import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
 import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.oauth2.jose.TestJwks;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadataClaimNames;
 import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
@@ -41,8 +47,11 @@ import org.springframework.security.oauth2.server.authorization.client.TestRegis
 import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.oauth2.server.authorization.test.SpringTestRule;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.test.web.servlet.MockMvc;
 
+import static org.hamcrest.CoreMatchers.hasItems;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -111,6 +120,17 @@ public class OAuth2AuthorizationServerMetadataTests {
 				.andReturn();
 	}
 
+	// gh-616
+	@Test
+	public void requestWhenAuthorizationServerMetadataRequestAndMetadataCustomizerSetThenReturnCustomMetadataResponse() throws Exception {
+		this.spring.register(AuthorizationServerConfigurationWithMetadataCustomizer.class).autowire();
+
+		this.mvc.perform(get(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI))
+				.andExpect(status().is2xxSuccessful())
+				.andExpect(jsonPath(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED,
+						hasItems("scope1", "scope2")));
+	}
+
 	@EnableWebSecurity
 	@Import(OAuth2AuthorizationServerConfiguration.class)
 	static class AuthorizationServerConfiguration {
@@ -139,6 +159,42 @@ public class OAuth2AuthorizationServerMetadataTests {
 		}
 	}
 
+	@Configuration
+	@EnableWebSecurity
+	static class AuthorizationServerConfigurationWithMetadataCustomizer extends AuthorizationServerConfiguration {
+
+		// @formatter:off
+		@Bean
+		public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
+			OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
+					new OAuth2AuthorizationServerConfigurer();
+			http.apply(authorizationServerConfigurer);
+
+			authorizationServerConfigurer
+					.authorizationServerMetadataEndpoint(authorizationServerMetadataEndpoint ->
+							authorizationServerMetadataEndpoint
+									.authorizationServerMetadataCustomizer(authorizationServerMetadataCustomizer()));
+
+			RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
+
+			http
+					.requestMatcher(endpointsMatcher)
+					.authorizeRequests(authorizeRequests ->
+							authorizeRequests.anyRequest().authenticated()
+					)
+					.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher));
+
+			return http.build();
+		}
+		// @formatter:on
+
+		private Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer() {
+			return (authorizationServerMetadata) ->
+					authorizationServerMetadata.scope("scope1").scope("scope2");
+		}
+
+	}
+
 	@EnableWebSecurity
 	@Import(OAuth2AuthorizationServerConfiguration.class)
 	static class AuthorizationServerConfigurationWithIssuerNotSet extends AuthorizationServerConfiguration {

+ 74 - 4
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 jakarta.servlet.ServletException;
 import jakarta.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<SecurityContext> jwkSource;
 	private static OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer;
 	private static AuthenticationConverter authenticationConverter;
+	private static Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer;
 	private static AuthenticationProvider authenticationProvider;
+	private static Consumer<List<AuthenticationProvider>> 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<List<AuthenticationConverter>> authenticationConvertersCaptor = ArgumentCaptor.forClass(List.class);
+		verify(authenticationConvertersConsumer).accept(authenticationConvertersCaptor.capture());
+		List<AuthenticationConverter> 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<List<AuthenticationProvider>> authenticationProvidersCaptor = ArgumentCaptor.forClass(List.class);
+		verify(authenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture());
+		List<AuthenticationProvider> 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<List<AuthenticationConverter>> authenticationConvertersCaptor = ArgumentCaptor.forClass(List.class);
+		verify(authenticationConvertersConsumer).accept(authenticationConvertersCaptor.capture());
+		List<AuthenticationConverter> 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<List<AuthenticationProvider>> authenticationProvidersCaptor = ArgumentCaptor.forClass(List.class);
+		verify(authenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture());
+		List<AuthenticationProvider> 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();

+ 27 - 0
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<OAuth2TokenClaimsContext> accessTokenCustomizer;
 	private static AuthenticationConverter authenticationConverter;
+	private static Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer;
 	private static AuthenticationProvider authenticationProvider;
+	private static Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer;
 	private static AuthenticationSuccessHandler authenticationSuccessHandler;
 	private static AuthenticationFailureHandler authenticationFailureHandler;
 	private static final HttpMessageConverter<OAuth2TokenIntrospection> 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<List<AuthenticationConverter>> authenticationConvertersCaptor = ArgumentCaptor.forClass(List.class);
+		verify(authenticationConvertersConsumer).accept(authenticationConvertersCaptor.capture());
+		List<AuthenticationConverter> authenticationConverters = authenticationConvertersCaptor.getValue();
+		assertThat(authenticationConverters).allMatch((converter) ->
+				converter == authenticationConverter ||
+						converter instanceof OAuth2TokenIntrospectionAuthenticationConverter);
+
 		verify(authenticationProvider).authenticate(eq(tokenIntrospectionAuthentication));
+
+		@SuppressWarnings("unchecked")
+		ArgumentCaptor<List<AuthenticationProvider>> authenticationProvidersCaptor = ArgumentCaptor.forClass(List.class);
+		verify(authenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture());
+		List<AuthenticationProvider> 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();

+ 29 - 0
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<SecurityContext> jwkSource;
 	private static AuthenticationConverter authenticationConverter;
+	private static Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer;
 	private static AuthenticationProvider authenticationProvider;
+	private static Consumer<List<AuthenticationProvider>> 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<List<AuthenticationConverter>> authenticationConvertersCaptor = ArgumentCaptor.forClass(List.class);
+		verify(authenticationConvertersConsumer).accept(authenticationConvertersCaptor.capture());
+		List<AuthenticationConverter> authenticationConverters = authenticationConvertersCaptor.getValue();
+		assertThat(authenticationConverters).allMatch((converter) ->
+				converter == authenticationConverter ||
+						converter instanceof OAuth2TokenRevocationAuthenticationConverter);
+
 		verify(authenticationProvider).authenticate(eq(tokenRevocationAuthentication));
+
+		@SuppressWarnings("unchecked")
+		ArgumentCaptor<List<AuthenticationProvider>> authenticationProvidersCaptor = ArgumentCaptor.forClass(List.class);
+		verify(authenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture());
+		List<AuthenticationProvider> 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();

+ 52 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java

@@ -24,6 +24,7 @@ import java.util.Base64;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Consumer;
 
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jose.jwk.source.JWKSource;
@@ -70,6 +71,7 @@ import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
 import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadataClaimNames;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
 import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
@@ -80,6 +82,7 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
 import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
 import org.springframework.security.oauth2.server.authorization.jackson2.TestingAuthenticationTokenMixin;
+import org.springframework.security.oauth2.server.authorization.oidc.OidcProviderConfiguration;
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.oauth2.server.authorization.test.SpringTestRule;
 import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
@@ -102,6 +105,7 @@ import org.springframework.web.util.UriComponentsBuilder;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.hasItems;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -197,6 +201,17 @@ public class OidcTests {
 				.andExpect(status().is2xxSuccessful());
 	}
 
+	// gh-616
+	@Test
+	public void requestWhenConfigurationRequestAndConfigurationCustomizerSetThenReturnCustomConfigurationResponse() throws Exception {
+		this.spring.register(AuthorizationServerConfigurationWithProviderConfigurationCustomizer.class).autowire();
+
+		this.mvc.perform(get(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI))
+				.andExpect(status().is2xxSuccessful())
+				.andExpect(jsonPath(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED,
+						hasItems(OidcScopes.OPENID, OidcScopes.PROFILE, OidcScopes.EMAIL)));
+	}
+
 	@Test
 	public void loadContextWhenIssuerNotValidUrlThenThrowException() {
 		assertThatThrownBy(
@@ -466,6 +481,43 @@ public class OidcTests {
 
 	}
 
+	@Configuration
+	@EnableWebSecurity
+	static class AuthorizationServerConfigurationWithProviderConfigurationCustomizer extends AuthorizationServerConfiguration {
+
+		// @formatter:off
+		@Bean
+		public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
+			OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
+					new OAuth2AuthorizationServerConfigurer();
+			http.apply(authorizationServerConfigurer);
+
+			authorizationServerConfigurer
+					.oidc(oidc ->
+							oidc.providerConfigurationEndpoint(providerConfigurationEndpoint ->
+									providerConfigurationEndpoint
+											.providerConfigurationCustomizer(providerConfigurationCustomizer())));
+
+			RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
+
+			http
+					.requestMatcher(endpointsMatcher)
+					.authorizeRequests(authorizeRequests ->
+							authorizeRequests.anyRequest().authenticated()
+					)
+					.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher));
+
+			return http.build();
+		}
+		// @formatter:on
+
+		private Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer() {
+			return (providerConfiguration) ->
+					providerConfiguration.scope(OidcScopes.PROFILE).scope(OidcScopes.EMAIL);
+		}
+
+	}
+
 	@EnableWebSecurity
 	@Import(OAuth2AuthorizationServerConfiguration.class)
 	static class AuthorizationServerConfigurationWithIssuer extends AuthorizationServerConfiguration {

+ 14 - 14
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java

@@ -31,6 +31,7 @@ import org.springframework.security.oauth2.server.authorization.settings.Authori
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -40,9 +41,11 @@ import static org.mockito.Mockito.verifyNoInteractions;
  * Tests for {@link OidcProviderConfigurationEndpointFilter}.
  *
  * @author Daniel Garnier-Moiroux
+ * @author Joe Grandja
  */
 public class OidcProviderConfigurationEndpointFilterTests {
 	private static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/.well-known/openid-configuration";
+	private final OidcProviderConfigurationEndpointFilter filter = new OidcProviderConfigurationEndpointFilter();
 
 	@After
 	public void cleanup() {
@@ -50,35 +53,34 @@ public class OidcProviderConfigurationEndpointFilterTests {
 	}
 
 	@Test
-	public void doFilterWhenNotConfigurationRequestThenNotProcessed() throws Exception {
-		AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder().build();
-		AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
-		OidcProviderConfigurationEndpointFilter filter = new OidcProviderConfigurationEndpointFilter();
+	public void setProviderConfigurationCustomizerWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.filter.setProviderConfigurationCustomizer(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("providerConfigurationCustomizer cannot be null");
+	}
 
+	@Test
+	public void doFilterWhenNotConfigurationRequestThenNotProcessed() throws Exception {
 		String requestUri = "/path";
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
 		request.setServletPath(requestUri);
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		FilterChain filterChain = mock(FilterChain.class);
 
-		filter.doFilter(request, response, filterChain);
+		this.filter.doFilter(request, response, filterChain);
 
 		verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
 	}
 
 	@Test
 	public void doFilterWhenConfigurationRequestPostThenNotProcessed() throws Exception {
-		AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder().build();
-		AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
-		OidcProviderConfigurationEndpointFilter filter = new OidcProviderConfigurationEndpointFilter();
-
 		String requestUri = DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI;
 		MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
 		request.setServletPath(requestUri);
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		FilterChain filterChain = mock(FilterChain.class);
 
-		filter.doFilter(request, response, filterChain);
+		this.filter.doFilter(request, response, filterChain);
 
 		verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
 	}
@@ -103,7 +105,6 @@ public class OidcProviderConfigurationEndpointFilterTests {
 				.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint)
 				.build();
 		AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
-		OidcProviderConfigurationEndpointFilter filter = new OidcProviderConfigurationEndpointFilter();
 
 		String requestUri = DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI;
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
@@ -111,7 +112,7 @@ public class OidcProviderConfigurationEndpointFilterTests {
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		FilterChain filterChain = mock(FilterChain.class);
 
-		filter.doFilter(request, response, filterChain);
+		this.filter.doFilter(request, response, filterChain);
 
 		verifyNoInteractions(filterChain);
 
@@ -140,7 +141,6 @@ public class OidcProviderConfigurationEndpointFilterTests {
 				.issuer("https://this is an invalid URL")
 				.build();
 		AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
-		OidcProviderConfigurationEndpointFilter filter = new OidcProviderConfigurationEndpointFilter();
 
 		String requestUri = DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI;
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
@@ -149,7 +149,7 @@ public class OidcProviderConfigurationEndpointFilterTests {
 		FilterChain filterChain = mock(FilterChain.class);
 
 		assertThatIllegalArgumentException()
-				.isThrownBy(() -> filter.doFilter(request, response, filterChain))
+				.isThrownBy(() -> this.filter.doFilter(request, response, filterChain))
 				.withMessage("issuer must be a valid URL");
 	}
 

+ 9 - 2
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java

@@ -152,7 +152,10 @@ public class JwtGeneratorTests {
 
 	@Test
 	public void generateWhenIdTokenTypeThenReturnJwt() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.scope(OidcScopes.OPENID)
+				.tokenSettings(TokenSettings.builder().idTokenSignatureAlgorithm(SignatureAlgorithm.ES256).build())
+				.build();
 		Map<String, Object> authenticationRequestAdditionalParameters = new HashMap<>();
 		authenticationRequestAdditionalParameters.put(OidcParameterNames.NONCE, "nonce");
 		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
@@ -202,7 +205,11 @@ public class JwtGeneratorTests {
 		verify(this.jwtEncoder).encode(jwtEncoderParametersCaptor.capture());
 
 		JwsHeader jwsHeader = jwtEncoderParametersCaptor.getValue().getJwsHeader();
-		assertThat(jwsHeader.getAlgorithm()).isEqualTo(SignatureAlgorithm.RS256);
+		if (OidcParameterNames.ID_TOKEN.equals(tokenContext.getTokenType().getValue())) {
+			assertThat(jwsHeader.getAlgorithm()).isEqualTo(tokenContext.getRegisteredClient().getTokenSettings().getIdTokenSignatureAlgorithm());
+		} else {
+			assertThat(jwsHeader.getAlgorithm()).isEqualTo(SignatureAlgorithm.RS256);
+		}
 
 		JwtClaimsSet jwtClaimsSet = jwtEncoderParametersCaptor.getValue().getClaims();
 		assertThat(jwtClaimsSet.getIssuer().toExternalForm()).isEqualTo(tokenContext.getAuthorizationServerContext().getIssuer());

+ 14 - 18
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java

@@ -31,6 +31,7 @@ import org.springframework.security.oauth2.server.authorization.settings.Authori
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -40,9 +41,11 @@ import static org.mockito.Mockito.verifyNoInteractions;
  * Tests for {@link OAuth2AuthorizationServerMetadataEndpointFilter}.
  *
  * @author Daniel Garnier-Moiroux
+ * @author Joe Grandja
  */
 public class OAuth2AuthorizationServerMetadataEndpointFilterTests {
 	private static final String DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI = "/.well-known/oauth-authorization-server";
+	private final OAuth2AuthorizationServerMetadataEndpointFilter filter = new OAuth2AuthorizationServerMetadataEndpointFilter();
 
 	@After
 	public void cleanup() {
@@ -50,39 +53,34 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests {
 	}
 
 	@Test
-	public void doFilterWhenNotAuthorizationServerMetadataRequestThenNotProcessed() throws Exception {
-		AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder()
-				.issuer("https://example.com")
-				.build();
-		AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
-		OAuth2AuthorizationServerMetadataEndpointFilter filter = new OAuth2AuthorizationServerMetadataEndpointFilter();
+	public void setAuthorizationServerMetadataCustomizerWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.filter.setAuthorizationServerMetadataCustomizer(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("authorizationServerMetadataCustomizer cannot be null");
+	}
 
+	@Test
+	public void doFilterWhenNotAuthorizationServerMetadataRequestThenNotProcessed() throws Exception {
 		String requestUri = "/path";
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
 		request.setServletPath(requestUri);
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		FilterChain filterChain = mock(FilterChain.class);
 
-		filter.doFilter(request, response, filterChain);
+		this.filter.doFilter(request, response, filterChain);
 
 		verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
 	}
 
 	@Test
 	public void doFilterWhenAuthorizationServerMetadataRequestPostThenNotProcessed() throws Exception {
-		AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder()
-				.issuer("https://example.com")
-				.build();
-		AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
-		OAuth2AuthorizationServerMetadataEndpointFilter filter = new OAuth2AuthorizationServerMetadataEndpointFilter();
-
 		String requestUri = DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI;
 		MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
 		request.setServletPath(requestUri);
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		FilterChain filterChain = mock(FilterChain.class);
 
-		filter.doFilter(request, response, filterChain);
+		this.filter.doFilter(request, response, filterChain);
 
 		verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
 	}
@@ -105,7 +103,6 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests {
 				.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint)
 				.build();
 		AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
-		OAuth2AuthorizationServerMetadataEndpointFilter filter = new OAuth2AuthorizationServerMetadataEndpointFilter();
 
 		String requestUri = DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI;
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
@@ -113,7 +110,7 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests {
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		FilterChain filterChain = mock(FilterChain.class);
 
-		filter.doFilter(request, response, filterChain);
+		this.filter.doFilter(request, response, filterChain);
 
 		verifyNoInteractions(filterChain);
 
@@ -139,7 +136,6 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests {
 				.issuer("https://this is an invalid URL")
 				.build();
 		AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
-		OAuth2AuthorizationServerMetadataEndpointFilter filter = new OAuth2AuthorizationServerMetadataEndpointFilter();
 
 		String requestUri = DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI;
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
@@ -149,7 +145,7 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests {
 
 
 		assertThatIllegalArgumentException()
-				.isThrownBy(() -> filter.doFilter(request, response, filterChain))
+				.isThrownBy(() -> this.filter.doFilter(request, response, filterChain))
 				.withMessage("issuer must be a valid URL");
 	}