Ver código fonte

Manual move of spring-projects/spring-authorization-server src/main

Issue gh-17880
Joe Grandja 3 semanas atrás
pai
commit
745e2153ed
100 arquivos alterados com 19346 adições e 0 exclusões
  1. 612 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java
  2. 108 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/InMemoryOAuth2AuthorizationConsentService.java
  3. 243 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/InMemoryOAuth2AuthorizationService.java
  4. 291 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationConsentService.java
  5. 853 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationService.java
  6. 575 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2Authorization.java
  7. 44 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationCode.java
  8. 224 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationConsent.java
  9. 55 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationConsentService.java
  10. 86 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadata.java
  11. 224 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java
  12. 158 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java
  13. 62 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationService.java
  14. 344 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenIntrospection.java
  15. 82 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenType.java
  16. 213 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/aot/hint/OAuth2AuthorizationServerBeanRegistrationAotProcessor.java
  17. 136 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/AbstractOAuth2AuthorizationCodeRequestAuthenticationToken.java
  18. 172 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/ClientSecretAuthenticationProvider.java
  19. 177 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/CodeVerifierAuthenticator.java
  20. 71 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/DPoPProofVerifier.java
  21. 172 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/JwtClientAssertionAuthenticationProvider.java
  22. 218 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/JwtClientAssertionDecoderFactory.java
  23. 111 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AccessTokenAuthenticationContext.java
  24. 150 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AccessTokenAuthenticationToken.java
  25. 109 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationContext.java
  26. 80 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationProviderUtils.java
  27. 358 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java
  28. 75 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationToken.java
  29. 56 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeGenerator.java
  30. 153 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationContext.java
  31. 74 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationException.java
  32. 508 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java
  33. 93 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationToken.java
  34. 312 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java
  35. 172 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContext.java
  36. 380 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationProvider.java
  37. 135 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationToken.java
  38. 95 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationGrantAuthenticationToken.java
  39. 107 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationContext.java
  40. 139 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationToken.java
  41. 107 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationContext.java
  42. 202 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java
  43. 61 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationToken.java
  44. 83 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationValidator.java
  45. 270 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationConsentAuthenticationProvider.java
  46. 106 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationConsentAuthenticationToken.java
  47. 281 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationRequestAuthenticationProvider.java
  48. 154 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationRequestAuthenticationToken.java
  49. 270 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProvider.java
  50. 60 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationToken.java
  51. 219 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationProvider.java
  52. 122 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationToken.java
  53. 184 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestAuthenticationProvider.java
  54. 109 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestAuthenticationToken.java
  55. 86 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestUri.java
  56. 336 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java
  57. 74 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationToken.java
  58. 71 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeActor.java
  59. 333 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeAuthenticationProvider.java
  60. 153 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeAuthenticationToken.java
  61. 85 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeCompositeAuthenticationToken.java
  62. 193 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProvider.java
  63. 141 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationToken.java
  64. 99 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenRevocationAuthenticationProvider.java
  65. 107 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenRevocationAuthenticationToken.java
  66. 38 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OidcPrompt.java
  67. 120 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/PublicClientAuthenticationProvider.java
  68. 190 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/X509ClientCertificateAuthenticationProvider.java
  69. 223 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/X509SelfSignedCertificateVerifier.java
  70. 118 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/InMemoryRegisteredClientRepository.java
  71. 436 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepository.java
  72. 638 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/RegisteredClient.java
  73. 59 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/RegisteredClientRepository.java
  74. 90 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configuration/OAuth2AuthorizationServerConfiguration.java
  75. 75 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configuration/RegisterMissingBeanPostProcessor.java
  76. 50 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/AbstractOAuth2Configurer.java
  77. 155 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/AuthorizationServerContextFilter.java
  78. 147 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/DefaultOAuth2TokenCustomizers.java
  79. 327 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationEndpointConfigurer.java
  80. 506 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java
  81. 119 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataEndpointConfigurer.java
  82. 288 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java
  83. 246 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ConfigurerUtils.java
  84. 273 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceAuthorizationEndpointConfigurer.java
  85. 325 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceVerificationEndpointConfigurer.java
  86. 266 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2PushedAuthorizationRequestEndpointConfigurer.java
  87. 280 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java
  88. 251 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionEndpointConfigurer.java
  89. 250 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationEndpointConfigurer.java
  90. 274 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationEndpointConfigurer.java
  91. 168 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java
  92. 237 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcLogoutEndpointConfigurer.java
  93. 117 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationEndpointConfigurer.java
  94. 286 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcUserInfoEndpointConfigurer.java
  95. 62 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/context/AuthorizationServerContext.java
  96. 61 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/context/AuthorizationServerContextHolder.java
  97. 62 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/context/Context.java
  98. 64 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/http/converter/HttpMessageConverters.java
  99. 191 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/http/converter/OAuth2AuthorizationServerMetadataHttpMessageConverter.java
  100. 221 0
      oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/http/converter/OAuth2TokenIntrospectionHttpMessageConverter.java

+ 612 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java

@@ -0,0 +1,612 @@
+/*
+ * Copyright 2020-2025 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;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.net.URI;
+import java.net.URL;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
+import org.springframework.util.Assert;
+
+/**
+ * A base representation of OAuth 2.0 Authorization Server metadata, returned by an
+ * endpoint defined in OAuth 2.0 Authorization Server Metadata and OpenID Connect
+ * Discovery 1.0. The metadata endpoint returns a set of claims an Authorization Server
+ * describes about its configuration.
+ *
+ * @author Daniel Garnier-Moiroux
+ * @author Joe Grandja
+ * @since 0.1.1
+ * @see OAuth2AuthorizationServerMetadataClaimAccessor
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-3.2">3.2.
+ * Authorization Server Metadata Response</a>
+ * @see <a target="_blank" href=
+ * "https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">4.2.
+ * OpenID Provider Configuration Response</a>
+ * @see <a target="_blank" href="https://www.rfc-editor.org/rfc/rfc8628.html#section-4">4.
+ * Device Authorization Grant Metadata</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc8705#section-3.3">3.3 Mutual-TLS Client
+ * Certificate-Bound Access Tokens Metadata</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc9449#section-5.1">5.1 OAuth 2.0 Demonstrating
+ * Proof of Possession (DPoP) Metadata</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc9126#name-authorization-server-metada">5.
+ * OAuth 2.0 Pushed Authorization Requests Metadata</a>
+ */
+public abstract class AbstractOAuth2AuthorizationServerMetadata
+		implements OAuth2AuthorizationServerMetadataClaimAccessor, Serializable {
+
+	@Serial
+	private static final long serialVersionUID = -8817963285912690443L;
+
+	private final Map<String, Object> claims;
+
+	protected AbstractOAuth2AuthorizationServerMetadata(Map<String, Object> claims) {
+		Assert.notEmpty(claims, "claims cannot be empty");
+		this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
+	}
+
+	/**
+	 * Returns the metadata as claims.
+	 * @return a {@code Map} of the metadata as claims
+	 */
+	@Override
+	public Map<String, Object> getClaims() {
+		return this.claims;
+	}
+
+	/**
+	 * A builder for subclasses of {@link AbstractOAuth2AuthorizationServerMetadata}.
+	 *
+	 * @param <T> the type of object
+	 * @param <B> the type of the builder
+	 */
+	protected abstract static class AbstractBuilder<T extends AbstractOAuth2AuthorizationServerMetadata, B extends AbstractBuilder<T, B>> {
+
+		private final Map<String, Object> claims = new LinkedHashMap<>();
+
+		protected AbstractBuilder() {
+		}
+
+		protected Map<String, Object> getClaims() {
+			return this.claims;
+		}
+
+		@SuppressWarnings("unchecked")
+		protected final B getThis() {
+			// avoid unchecked casts in subclasses by using "getThis()" instead of "(B)
+			// this"
+			return (B) this;
+		}
+
+		/**
+		 * Use this {@code issuer} in the resulting
+		 * {@link AbstractOAuth2AuthorizationServerMetadata}, REQUIRED.
+		 * @param issuer the {@code URL} of the Authorization Server's Issuer Identifier
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B issuer(String issuer) {
+			return claim(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, issuer);
+		}
+
+		/**
+		 * Use this {@code authorization_endpoint} in the resulting
+		 * {@link AbstractOAuth2AuthorizationServerMetadata}, REQUIRED.
+		 * @param authorizationEndpoint the {@code URL} of the OAuth 2.0 Authorization
+		 * Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B authorizationEndpoint(String authorizationEndpoint) {
+			return claim(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, authorizationEndpoint);
+		}
+
+		/**
+		 * Use this {@code pushed_authorization_request_endpoint} in the resulting
+		 * {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 * @param pushedAuthorizationRequestEndpoint the {@code URL} of the OAuth 2.0
+		 * Pushed Authorization Request Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 * @since 1.5
+		 */
+		public B pushedAuthorizationRequestEndpoint(String pushedAuthorizationRequestEndpoint) {
+			return claim(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT,
+					pushedAuthorizationRequestEndpoint);
+		}
+
+		/**
+		 * Use this {@code device_authorization_endpoint} in the resulting
+		 * {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 * @param deviceAuthorizationEndpoint the {@code URL} of the OAuth 2.0 Device
+		 * Authorization Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 * @since 1.1
+		 */
+		public B deviceAuthorizationEndpoint(String deviceAuthorizationEndpoint) {
+			return claim(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT,
+					deviceAuthorizationEndpoint);
+		}
+
+		/**
+		 * Use this {@code token_endpoint} in the resulting
+		 * {@link AbstractOAuth2AuthorizationServerMetadata}, REQUIRED.
+		 * @param tokenEndpoint the {@code URL} of the OAuth 2.0 Token Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B tokenEndpoint(String tokenEndpoint) {
+			return claim(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, tokenEndpoint);
+		}
+
+		/**
+		 * Add this client authentication method to the collection of
+		 * {@code token_endpoint_auth_methods_supported} in the resulting
+		 * {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 * @param authenticationMethod the client authentication method supported by the
+		 * OAuth 2.0 Token Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B tokenEndpointAuthenticationMethod(String authenticationMethod) {
+			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED,
+					authenticationMethod);
+			return getThis();
+		}
+
+		/**
+		 * A {@code Consumer} of the client authentication method(s) allowing the ability
+		 * to add, replace, or remove.
+		 * @param authenticationMethodsConsumer a {@code Consumer} of the client
+		 * authentication method(s) supported by the OAuth 2.0 Token Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B tokenEndpointAuthenticationMethods(Consumer<List<String>> authenticationMethodsConsumer) {
+			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED,
+					authenticationMethodsConsumer);
+			return getThis();
+		}
+
+		/**
+		 * Use this {@code jwks_uri} in the resulting
+		 * {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 * @param jwkSetUrl the {@code URL} of the JSON Web Key Set
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B jwkSetUrl(String jwkSetUrl) {
+			return claim(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, jwkSetUrl);
+		}
+
+		/**
+		 * Add this OAuth 2.0 {@code scope} to the collection of {@code scopes_supported}
+		 * in the resulting {@link AbstractOAuth2AuthorizationServerMetadata},
+		 * RECOMMENDED.
+		 * @param scope the OAuth 2.0 {@code scope} value supported
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B scope(String scope) {
+			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, scope);
+			return getThis();
+		}
+
+		/**
+		 * A {@code Consumer} of the OAuth 2.0 {@code scope} values supported allowing the
+		 * ability to add, replace, or remove.
+		 * @param scopesConsumer a {@code Consumer} of the OAuth 2.0 {@code scope} values
+		 * supported
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B scopes(Consumer<List<String>> scopesConsumer) {
+			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, scopesConsumer);
+			return getThis();
+		}
+
+		/**
+		 * Add this OAuth 2.0 {@code response_type} to the collection of
+		 * {@code response_types_supported} in the resulting
+		 * {@link AbstractOAuth2AuthorizationServerMetadata}, REQUIRED.
+		 * @param responseType the OAuth 2.0 {@code response_type} value supported
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B responseType(String responseType) {
+			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, responseType);
+			return getThis();
+		}
+
+		/**
+		 * A {@code Consumer} of the OAuth 2.0 {@code response_type} values supported
+		 * allowing the ability to add, replace, or remove.
+		 * @param responseTypesConsumer a {@code Consumer} of the OAuth 2.0
+		 * {@code response_type} values supported
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B responseTypes(Consumer<List<String>> responseTypesConsumer) {
+			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED,
+					responseTypesConsumer);
+			return getThis();
+		}
+
+		/**
+		 * Add this OAuth 2.0 {@code grant_type} to the collection of
+		 * {@code grant_types_supported} in the resulting
+		 * {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 * @param grantType the OAuth 2.0 {@code grant_type} value supported
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B grantType(String grantType) {
+			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, grantType);
+			return getThis();
+		}
+
+		/**
+		 * A {@code Consumer} of the OAuth 2.0 {@code grant_type} values supported
+		 * allowing the ability to add, replace, or remove.
+		 * @param grantTypesConsumer a {@code Consumer} of the OAuth 2.0
+		 * {@code grant_type} values supported
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B grantTypes(Consumer<List<String>> grantTypesConsumer) {
+			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, grantTypesConsumer);
+			return getThis();
+		}
+
+		/**
+		 * Use this {@code revocation_endpoint} in the resulting
+		 * {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 * @param tokenRevocationEndpoint the {@code URL} of the OAuth 2.0 Token
+		 * Revocation Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B tokenRevocationEndpoint(String tokenRevocationEndpoint) {
+			return claim(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, tokenRevocationEndpoint);
+		}
+
+		/**
+		 * Add this client authentication method to the collection of
+		 * {@code revocation_endpoint_auth_methods_supported} in the resulting
+		 * {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 * @param authenticationMethod the client authentication method supported by the
+		 * OAuth 2.0 Token Revocation Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B tokenRevocationEndpointAuthenticationMethod(String authenticationMethod) {
+			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED,
+					authenticationMethod);
+			return getThis();
+		}
+
+		/**
+		 * A {@code Consumer} of the client authentication method(s) allowing the ability
+		 * to add, replace, or remove.
+		 * @param authenticationMethodsConsumer a {@code Consumer} of the client
+		 * authentication method(s) supported by the OAuth 2.0 Token Revocation Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B tokenRevocationEndpointAuthenticationMethods(Consumer<List<String>> authenticationMethodsConsumer) {
+			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED,
+					authenticationMethodsConsumer);
+			return getThis();
+		}
+
+		/**
+		 * Use this {@code introspection_endpoint} in the resulting
+		 * {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 * @param tokenIntrospectionEndpoint the {@code URL} of the OAuth 2.0 Token
+		 * Introspection Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B tokenIntrospectionEndpoint(String tokenIntrospectionEndpoint) {
+			return claim(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT,
+					tokenIntrospectionEndpoint);
+		}
+
+		/**
+		 * Add this client authentication method to the collection of
+		 * {@code introspection_endpoint_auth_methods_supported} in the resulting
+		 * {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 * @param authenticationMethod the client authentication method supported by the
+		 * OAuth 2.0 Token Introspection Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B tokenIntrospectionEndpointAuthenticationMethod(String authenticationMethod) {
+			addClaimToClaimList(
+					OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED,
+					authenticationMethod);
+			return getThis();
+		}
+
+		/**
+		 * A {@code Consumer} of the client authentication method(s) allowing the ability
+		 * to add, replace, or remove.
+		 * @param authenticationMethodsConsumer a {@code Consumer} of the client
+		 * authentication method(s) supported by the OAuth 2.0 Token Introspection
+		 * Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B tokenIntrospectionEndpointAuthenticationMethods(Consumer<List<String>> authenticationMethodsConsumer) {
+			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED,
+					authenticationMethodsConsumer);
+			return getThis();
+		}
+
+		/**
+		 * Use this {@code registration_endpoint} in the resulting
+		 * {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 * @param clientRegistrationEndpoint the {@code URL} of the OAuth 2.0 Dynamic
+		 * Client Registration Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 * @since 0.4.0
+		 */
+		public B clientRegistrationEndpoint(String clientRegistrationEndpoint) {
+			return claim(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT, clientRegistrationEndpoint);
+		}
+
+		/**
+		 * Add this Proof Key for Code Exchange (PKCE) {@code code_challenge_method} to
+		 * the collection of {@code code_challenge_methods_supported} in the resulting
+		 * {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 * @param codeChallengeMethod the {@code code_challenge_method} value supported
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B codeChallengeMethod(String codeChallengeMethod) {
+			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED,
+					codeChallengeMethod);
+			return getThis();
+		}
+
+		/**
+		 * A {@code Consumer} of the Proof Key for Code Exchange (PKCE)
+		 * {@code code_challenge_method} values supported allowing the ability to add,
+		 * replace, or remove.
+		 * @param codeChallengeMethodsConsumer a {@code Consumer} of the
+		 * {@code code_challenge_method} values supported
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B codeChallengeMethods(Consumer<List<String>> codeChallengeMethodsConsumer) {
+			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED,
+					codeChallengeMethodsConsumer);
+			return getThis();
+		}
+
+		/**
+		 * Use this {@code tls_client_certificate_bound_access_tokens} in the resulting
+		 * {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 * @param tlsClientCertificateBoundAccessTokens {@code true} to indicate support
+		 * for mutual-TLS client certificate-bound access tokens
+		 * @return the {@link AbstractBuilder} for further configuration
+		 * @since 1.3
+		 */
+		public B tlsClientCertificateBoundAccessTokens(boolean tlsClientCertificateBoundAccessTokens) {
+			return claim(OAuth2AuthorizationServerMetadataClaimNames.TLS_CLIENT_CERTIFICATE_BOUND_ACCESS_TOKENS,
+					tlsClientCertificateBoundAccessTokens);
+		}
+
+		/**
+		 * Add a {@link JwsAlgorithms JSON Web Signature (JWS) algorithm} to the
+		 * collection of {@code dpop_signing_alg_values_supported} in the resulting
+		 * {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 * @param dPoPSigningAlgorithm the {@link JwsAlgorithms JSON Web Signature (JWS)
+		 * algorithm} supported for DPoP Proof JWTs
+		 * @return the {@link AbstractBuilder} for further configuration
+		 * @since 1.5
+		 */
+		public B dPoPSigningAlgorithm(String dPoPSigningAlgorithm) {
+			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.DPOP_SIGNING_ALG_VALUES_SUPPORTED,
+					dPoPSigningAlgorithm);
+			return getThis();
+		}
+
+		/**
+		 * A {@code Consumer} of the {@link JwsAlgorithms JSON Web Signature (JWS)
+		 * algorithms} supported for DPoP Proof JWTs allowing the ability to add, replace,
+		 * or remove.
+		 * @param dPoPSigningAlgorithmsConsumer a {@code Consumer} of the
+		 * {@link JwsAlgorithms JSON Web Signature (JWS) algorithms} supported for DPoP
+		 * Proof JWTs
+		 * @return the {@link AbstractBuilder} for further configuration
+		 * @since 1.5
+		 */
+		public B dPoPSigningAlgorithms(Consumer<List<String>> dPoPSigningAlgorithmsConsumer) {
+			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.DPOP_SIGNING_ALG_VALUES_SUPPORTED,
+					dPoPSigningAlgorithmsConsumer);
+			return getThis();
+		}
+
+		/**
+		 * Use this claim in the resulting
+		 * {@link AbstractOAuth2AuthorizationServerMetadata}.
+		 * @param name the claim name
+		 * @param value the claim value
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B claim(String name, Object value) {
+			Assert.hasText(name, "name cannot be empty");
+			Assert.notNull(value, "value cannot be null");
+			this.claims.put(name, value);
+			return getThis();
+		}
+
+		/**
+		 * Provides access to every {@link #claim(String, Object)} declared so far with
+		 * the possibility to add, replace, or remove.
+		 * @param claimsConsumer a {@code Consumer} of the claims
+		 * @return the {@link AbstractBuilder} for further configurations
+		 */
+		public B claims(Consumer<Map<String, Object>> claimsConsumer) {
+			claimsConsumer.accept(this.claims);
+			return getThis();
+		}
+
+		/**
+		 * Creates the {@link AbstractOAuth2AuthorizationServerMetadata}.
+		 * @return the {@link AbstractOAuth2AuthorizationServerMetadata}
+		 */
+		public abstract T build();
+
+		protected void validate() {
+			Assert.notNull(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.ISSUER),
+					"issuer cannot be null");
+			validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.ISSUER),
+					"issuer must be a valid URL");
+			Assert.notNull(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT),
+					"authorizationEndpoint cannot be null");
+			validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT),
+					"authorizationEndpoint must be a valid URL");
+			if (getClaims()
+				.get(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT) != null) {
+				validateURL(
+						getClaims()
+							.get(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT),
+						"pushedAuthorizationRequestEndpoint must be a valid URL");
+			}
+			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT) != null) {
+				validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT),
+						"deviceAuthorizationEndpoint must be a valid URL");
+			}
+			Assert.notNull(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT),
+					"tokenEndpoint cannot be null");
+			validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT),
+					"tokenEndpoint must be a valid URL");
+			if (getClaims()
+				.get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED) != null) {
+				Assert.isInstanceOf(List.class,
+						getClaims()
+							.get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED),
+						"tokenEndpointAuthenticationMethods must be of type List");
+				Assert.notEmpty(
+						(List<?>) getClaims()
+							.get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED),
+						"tokenEndpointAuthenticationMethods cannot be empty");
+			}
+			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI) != null) {
+				validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI),
+						"jwksUri must be a valid URL");
+			}
+			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED) != null) {
+				Assert.isInstanceOf(List.class,
+						getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED),
+						"scopes must be of type List");
+				Assert.notEmpty((List<?>) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED),
+						"scopes cannot be empty");
+			}
+			Assert.notNull(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED),
+					"responseTypes cannot be null");
+			Assert.isInstanceOf(List.class,
+					getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED),
+					"responseTypes must be of type List");
+			Assert.notEmpty(
+					(List<?>) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED),
+					"responseTypes cannot be empty");
+			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED) != null) {
+				Assert.isInstanceOf(List.class,
+						getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED),
+						"grantTypes must be of type List");
+				Assert.notEmpty(
+						(List<?>) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED),
+						"grantTypes cannot be empty");
+			}
+			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT) != null) {
+				validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT),
+						"tokenRevocationEndpoint must be a valid URL");
+			}
+			if (getClaims()
+				.get(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED) != null) {
+				Assert.isInstanceOf(List.class,
+						getClaims().get(
+								OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED),
+						"tokenRevocationEndpointAuthenticationMethods must be of type List");
+				Assert.notEmpty(
+						(List<?>) getClaims().get(
+								OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED),
+						"tokenRevocationEndpointAuthenticationMethods cannot be empty");
+			}
+			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT) != null) {
+				validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT),
+						"tokenIntrospectionEndpoint must be a valid URL");
+			}
+			if (getClaims().get(
+					OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED) != null) {
+				Assert.isInstanceOf(List.class, getClaims()
+					.get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED),
+						"tokenIntrospectionEndpointAuthenticationMethods must be of type List");
+				Assert.notEmpty((List<?>) getClaims()
+					.get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED),
+						"tokenIntrospectionEndpointAuthenticationMethods cannot be empty");
+			}
+			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT) != null) {
+				validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT),
+						"clientRegistrationEndpoint must be a valid URL");
+			}
+			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED) != null) {
+				Assert.isInstanceOf(List.class,
+						getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED),
+						"codeChallengeMethods must be of type List");
+				Assert.notEmpty(
+						(List<?>) getClaims()
+							.get(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED),
+						"codeChallengeMethods cannot be empty");
+			}
+			if (getClaims()
+				.get(OAuth2AuthorizationServerMetadataClaimNames.DPOP_SIGNING_ALG_VALUES_SUPPORTED) != null) {
+				Assert.isInstanceOf(List.class,
+						getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.DPOP_SIGNING_ALG_VALUES_SUPPORTED),
+						"dPoPSigningAlgorithms must be of type List");
+				Assert.notEmpty(
+						(List<?>) getClaims()
+							.get(OAuth2AuthorizationServerMetadataClaimNames.DPOP_SIGNING_ALG_VALUES_SUPPORTED),
+						"dPoPSigningAlgorithms cannot be empty");
+			}
+		}
+
+		@SuppressWarnings("unchecked")
+		private void addClaimToClaimList(String name, String value) {
+			Assert.hasText(name, "name cannot be empty");
+			Assert.notNull(value, "value cannot be null");
+			getClaims().computeIfAbsent(name, (k) -> new LinkedList<String>());
+			((List<String>) getClaims().get(name)).add(value);
+		}
+
+		@SuppressWarnings("unchecked")
+		private void acceptClaimValues(String name, Consumer<List<String>> valuesConsumer) {
+			Assert.hasText(name, "name cannot be empty");
+			Assert.notNull(valuesConsumer, "valuesConsumer cannot be null");
+			getClaims().computeIfAbsent(name, (k) -> new LinkedList<String>());
+			List<String> values = (List<String>) getClaims().get(name);
+			valuesConsumer.accept(values);
+		}
+
+		protected static void validateURL(Object url, String errorMessage) {
+			if (URL.class.isAssignableFrom(url.getClass())) {
+				return;
+			}
+
+			try {
+				new URI(url.toString()).toURL();
+			}
+			catch (Exception ex) {
+				throw new IllegalArgumentException(errorMessage, ex);
+			}
+		}
+
+	}
+
+}

+ 108 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/InMemoryOAuth2AuthorizationConsentService.java

@@ -0,0 +1,108 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link OAuth2AuthorizationConsentService} that stores
+ * {@link OAuth2AuthorizationConsent}'s in-memory.
+ *
+ * <p>
+ * <b>NOTE:</b> This implementation should ONLY be used during development/testing.
+ *
+ * @author Daniel Garnier-Moiroux
+ * @since 0.1.2
+ * @see OAuth2AuthorizationConsentService
+ */
+public final class InMemoryOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
+
+	private final Map<Integer, OAuth2AuthorizationConsent> authorizationConsents = new ConcurrentHashMap<>();
+
+	/**
+	 * Constructs an {@code InMemoryOAuth2AuthorizationConsentService}.
+	 */
+	public InMemoryOAuth2AuthorizationConsentService() {
+		this(Collections.emptyList());
+	}
+
+	/**
+	 * Constructs an {@code InMemoryOAuth2AuthorizationConsentService} using the provided
+	 * parameters.
+	 * @param authorizationConsents the authorization consent(s)
+	 */
+	public InMemoryOAuth2AuthorizationConsentService(OAuth2AuthorizationConsent... authorizationConsents) {
+		this(Arrays.asList(authorizationConsents));
+	}
+
+	/**
+	 * Constructs an {@code InMemoryOAuth2AuthorizationConsentService} using the provided
+	 * parameters.
+	 * @param authorizationConsents the authorization consent(s)
+	 */
+	public InMemoryOAuth2AuthorizationConsentService(List<OAuth2AuthorizationConsent> authorizationConsents) {
+		Assert.notNull(authorizationConsents, "authorizationConsents cannot be null");
+		authorizationConsents.forEach((authorizationConsent) -> {
+			Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
+			int id = getId(authorizationConsent);
+			Assert.isTrue(!this.authorizationConsents.containsKey(id),
+					"The authorizationConsent must be unique. Found duplicate, with registered client id: ["
+							+ authorizationConsent.getRegisteredClientId() + "] and principal name: ["
+							+ authorizationConsent.getPrincipalName() + "]");
+			this.authorizationConsents.put(id, authorizationConsent);
+		});
+	}
+
+	@Override
+	public void save(OAuth2AuthorizationConsent authorizationConsent) {
+		Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
+		int id = getId(authorizationConsent);
+		this.authorizationConsents.put(id, authorizationConsent);
+	}
+
+	@Override
+	public void remove(OAuth2AuthorizationConsent authorizationConsent) {
+		Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
+		int id = getId(authorizationConsent);
+		this.authorizationConsents.remove(id, authorizationConsent);
+	}
+
+	@Override
+	@Nullable
+	public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
+		Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
+		Assert.hasText(principalName, "principalName cannot be empty");
+		int id = getId(registeredClientId, principalName);
+		return this.authorizationConsents.get(id);
+	}
+
+	private static int getId(String registeredClientId, String principalName) {
+		return Objects.hash(registeredClientId, principalName);
+	}
+
+	private static int getId(OAuth2AuthorizationConsent authorizationConsent) {
+		return getId(authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName());
+	}
+
+}

+ 243 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/InMemoryOAuth2AuthorizationService.java

@@ -0,0 +1,243 @@
+/*
+ * Copyright 2020-2023 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;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2DeviceCode;
+import org.springframework.security.oauth2.core.OAuth2RefreshToken;
+import org.springframework.security.oauth2.core.OAuth2UserCode;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link OAuth2AuthorizationService} that stores {@link OAuth2Authorization}'s
+ * in-memory.
+ *
+ * <p>
+ * <b>NOTE:</b> This implementation should ONLY be used during development/testing.
+ *
+ * @author Krisztian Toth
+ * @author Joe Grandja
+ * @since 0.0.1
+ * @see OAuth2AuthorizationService
+ */
+public final class InMemoryOAuth2AuthorizationService implements OAuth2AuthorizationService {
+
+	private int maxInitializedAuthorizations = 100;
+
+	/*
+	 * Stores "initialized" (uncompleted) authorizations, where an access token has not
+	 * yet been granted. This state occurs with the authorization_code grant flow during
+	 * the user consent step OR when the code is returned in the authorization response
+	 * but the access token request is not yet initiated.
+	 */
+	private Map<String, OAuth2Authorization> initializedAuthorizations = Collections
+		.synchronizedMap(new MaxSizeHashMap<>(this.maxInitializedAuthorizations));
+
+	/*
+	 * Stores "completed" authorizations, where an access token has been granted.
+	 */
+	private final Map<String, OAuth2Authorization> authorizations = new ConcurrentHashMap<>();
+
+	/*
+	 * Constructor used for testing only.
+	 */
+	InMemoryOAuth2AuthorizationService(int maxInitializedAuthorizations) {
+		this.maxInitializedAuthorizations = maxInitializedAuthorizations;
+		this.initializedAuthorizations = Collections
+			.synchronizedMap(new MaxSizeHashMap<>(this.maxInitializedAuthorizations));
+	}
+
+	/**
+	 * Constructs an {@code InMemoryOAuth2AuthorizationService}.
+	 */
+	public InMemoryOAuth2AuthorizationService() {
+		this(Collections.emptyList());
+	}
+
+	/**
+	 * Constructs an {@code InMemoryOAuth2AuthorizationService} using the provided
+	 * parameters.
+	 * @param authorizations the authorization(s)
+	 */
+	public InMemoryOAuth2AuthorizationService(OAuth2Authorization... authorizations) {
+		this(Arrays.asList(authorizations));
+	}
+
+	/**
+	 * Constructs an {@code InMemoryOAuth2AuthorizationService} using the provided
+	 * parameters.
+	 * @param authorizations the authorization(s)
+	 */
+	public InMemoryOAuth2AuthorizationService(List<OAuth2Authorization> authorizations) {
+		Assert.notNull(authorizations, "authorizations cannot be null");
+		authorizations.forEach((authorization) -> {
+			Assert.notNull(authorization, "authorization cannot be null");
+			Assert.isTrue(!this.authorizations.containsKey(authorization.getId()),
+					"The authorization must be unique. Found duplicate identifier: " + authorization.getId());
+			this.authorizations.put(authorization.getId(), authorization);
+		});
+	}
+
+	@Override
+	public void save(OAuth2Authorization authorization) {
+		Assert.notNull(authorization, "authorization cannot be null");
+		if (isComplete(authorization)) {
+			this.authorizations.put(authorization.getId(), authorization);
+		}
+		else {
+			this.initializedAuthorizations.put(authorization.getId(), authorization);
+		}
+	}
+
+	@Override
+	public void remove(OAuth2Authorization authorization) {
+		Assert.notNull(authorization, "authorization cannot be null");
+		if (isComplete(authorization)) {
+			this.authorizations.remove(authorization.getId(), authorization);
+		}
+		else {
+			this.initializedAuthorizations.remove(authorization.getId(), authorization);
+		}
+	}
+
+	@Nullable
+	@Override
+	public OAuth2Authorization findById(String id) {
+		Assert.hasText(id, "id cannot be empty");
+		OAuth2Authorization authorization = this.authorizations.get(id);
+		return (authorization != null) ? authorization : this.initializedAuthorizations.get(id);
+	}
+
+	@Nullable
+	@Override
+	public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
+		Assert.hasText(token, "token cannot be empty");
+		for (OAuth2Authorization authorization : this.authorizations.values()) {
+			if (hasToken(authorization, token, tokenType)) {
+				return authorization;
+			}
+		}
+		for (OAuth2Authorization authorization : this.initializedAuthorizations.values()) {
+			if (hasToken(authorization, token, tokenType)) {
+				return authorization;
+			}
+		}
+		return null;
+	}
+
+	private static boolean isComplete(OAuth2Authorization authorization) {
+		return authorization.getAccessToken() != null;
+	}
+
+	private static boolean hasToken(OAuth2Authorization authorization, String token,
+			@Nullable OAuth2TokenType tokenType) {
+		// @formatter:off
+		if (tokenType == null) {
+			return matchesState(authorization, token) ||
+					matchesAuthorizationCode(authorization, token) ||
+					matchesAccessToken(authorization, token) ||
+					matchesIdToken(authorization, token) ||
+					matchesRefreshToken(authorization, token) ||
+					matchesDeviceCode(authorization, token) ||
+					matchesUserCode(authorization, token);
+		}
+		else if (OAuth2ParameterNames.STATE.equals(tokenType.getValue())) {
+			return matchesState(authorization, token);
+		}
+		else if (OAuth2ParameterNames.CODE.equals(tokenType.getValue())) {
+			return matchesAuthorizationCode(authorization, token);
+		}
+		else if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenType)) {
+			return matchesAccessToken(authorization, token);
+		}
+		else if (OidcParameterNames.ID_TOKEN.equals(tokenType.getValue())) {
+			return matchesIdToken(authorization, token);
+		}
+		else if (OAuth2TokenType.REFRESH_TOKEN.equals(tokenType)) {
+			return matchesRefreshToken(authorization, token);
+		}
+		else if (OAuth2ParameterNames.DEVICE_CODE.equals(tokenType.getValue())) {
+			return matchesDeviceCode(authorization, token);
+		}
+		else if (OAuth2ParameterNames.USER_CODE.equals(tokenType.getValue())) {
+			return matchesUserCode(authorization, token);
+		}
+		// @formatter:on
+		return false;
+	}
+
+	private static boolean matchesState(OAuth2Authorization authorization, String token) {
+		return token.equals(authorization.getAttribute(OAuth2ParameterNames.STATE));
+	}
+
+	private static boolean matchesAuthorizationCode(OAuth2Authorization authorization, String token) {
+		OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
+			.getToken(OAuth2AuthorizationCode.class);
+		return authorizationCode != null && authorizationCode.getToken().getTokenValue().equals(token);
+	}
+
+	private static boolean matchesAccessToken(OAuth2Authorization authorization, String token) {
+		OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getToken(OAuth2AccessToken.class);
+		return accessToken != null && accessToken.getToken().getTokenValue().equals(token);
+	}
+
+	private static boolean matchesRefreshToken(OAuth2Authorization authorization, String token) {
+		OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken = authorization.getToken(OAuth2RefreshToken.class);
+		return refreshToken != null && refreshToken.getToken().getTokenValue().equals(token);
+	}
+
+	private static boolean matchesIdToken(OAuth2Authorization authorization, String token) {
+		OAuth2Authorization.Token<OidcIdToken> idToken = authorization.getToken(OidcIdToken.class);
+		return idToken != null && idToken.getToken().getTokenValue().equals(token);
+	}
+
+	private static boolean matchesDeviceCode(OAuth2Authorization authorization, String token) {
+		OAuth2Authorization.Token<OAuth2DeviceCode> deviceCode = authorization.getToken(OAuth2DeviceCode.class);
+		return deviceCode != null && deviceCode.getToken().getTokenValue().equals(token);
+	}
+
+	private static boolean matchesUserCode(OAuth2Authorization authorization, String token) {
+		OAuth2Authorization.Token<OAuth2UserCode> userCode = authorization.getToken(OAuth2UserCode.class);
+		return userCode != null && userCode.getToken().getTokenValue().equals(token);
+	}
+
+	private static final class MaxSizeHashMap<K, V> extends LinkedHashMap<K, V> {
+
+		private final int maxSize;
+
+		private MaxSizeHashMap(int maxSize) {
+			this.maxSize = maxSize;
+		}
+
+		@Override
+		protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+			return size() > this.maxSize;
+		}
+
+	}
+
+}

+ 291 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationConsentService.java

@@ -0,0 +1,291 @@
+/*
+ * Copyright 2020-2023 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;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.RuntimeHintsRegistrar;
+import org.springframework.context.annotation.ImportRuntimeHints;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.dao.DataRetrievalFailureException;
+import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
+import org.springframework.jdbc.core.JdbcOperations;
+import org.springframework.jdbc.core.PreparedStatementSetter;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.SqlParameterValue;
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * A JDBC implementation of an {@link OAuth2AuthorizationConsentService} that uses a
+ * {@link JdbcOperations} for {@link OAuth2AuthorizationConsent} persistence.
+ *
+ * <p>
+ * <b>IMPORTANT:</b> This {@code OAuth2AuthorizationConsentService} depends on the table
+ * definition described in
+ * "classpath:org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql"
+ * and therefore MUST be defined in the database schema.
+ *
+ * <p>
+ * <b>NOTE:</b> This {@code OAuth2AuthorizationConsentService} is a simplified JDBC
+ * implementation that MAY be used in a production environment. However, it does have
+ * limitations as it likely won't perform well in an environment requiring high
+ * throughput. The expectation is that the consuming application will provide their own
+ * implementation of {@code OAuth2AuthorizationConsentService} that meets the performance
+ * requirements for its deployment environment.
+ *
+ * @author Ovidiu Popa
+ * @author Josh Long
+ * @since 0.1.2
+ * @see OAuth2AuthorizationConsentService
+ * @see OAuth2AuthorizationConsent
+ * @see JdbcOperations
+ * @see RowMapper
+ */
+@ImportRuntimeHints(JdbcOAuth2AuthorizationConsentService.JdbcOAuth2AuthorizationConsentServiceRuntimeHintsRegistrar.class)
+public class JdbcOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
+
+	// @formatter:off
+	private static final String COLUMN_NAMES = "registered_client_id, "
+			+ "principal_name, "
+			+ "authorities";
+	// @formatter:on
+
+	private static final String TABLE_NAME = "oauth2_authorization_consent";
+
+	private static final String PK_FILTER = "registered_client_id = ? AND principal_name = ?";
+
+	// @formatter:off
+	private static final String LOAD_AUTHORIZATION_CONSENT_SQL = "SELECT " + COLUMN_NAMES
+			+ " FROM " + TABLE_NAME
+			+ " WHERE " + PK_FILTER;
+	// @formatter:on
+
+	// @formatter:off
+	private static final String SAVE_AUTHORIZATION_CONSENT_SQL = "INSERT INTO " + TABLE_NAME
+			+ " (" + COLUMN_NAMES + ") VALUES (?, ?, ?)";
+	// @formatter:on
+
+	// @formatter:off
+	private static final String UPDATE_AUTHORIZATION_CONSENT_SQL = "UPDATE " + TABLE_NAME
+			+ " SET authorities = ?"
+			+ " WHERE " + PK_FILTER;
+	// @formatter:on
+
+	private static final String REMOVE_AUTHORIZATION_CONSENT_SQL = "DELETE FROM " + TABLE_NAME + " WHERE " + PK_FILTER;
+
+	private final JdbcOperations jdbcOperations;
+
+	private RowMapper<OAuth2AuthorizationConsent> authorizationConsentRowMapper;
+
+	private Function<OAuth2AuthorizationConsent, List<SqlParameterValue>> authorizationConsentParametersMapper;
+
+	/**
+	 * Constructs a {@code JdbcOAuth2AuthorizationConsentService} using the provided
+	 * parameters.
+	 * @param jdbcOperations the JDBC operations
+	 * @param registeredClientRepository the registered client repository
+	 */
+	public JdbcOAuth2AuthorizationConsentService(JdbcOperations jdbcOperations,
+			RegisteredClientRepository registeredClientRepository) {
+		Assert.notNull(jdbcOperations, "jdbcOperations cannot be null");
+		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+		this.jdbcOperations = jdbcOperations;
+		this.authorizationConsentRowMapper = new OAuth2AuthorizationConsentRowMapper(registeredClientRepository);
+		this.authorizationConsentParametersMapper = new OAuth2AuthorizationConsentParametersMapper();
+	}
+
+	@Override
+	public void save(OAuth2AuthorizationConsent authorizationConsent) {
+		Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
+		OAuth2AuthorizationConsent existingAuthorizationConsent = findById(authorizationConsent.getRegisteredClientId(),
+				authorizationConsent.getPrincipalName());
+		if (existingAuthorizationConsent == null) {
+			insertAuthorizationConsent(authorizationConsent);
+		}
+		else {
+			updateAuthorizationConsent(authorizationConsent);
+		}
+	}
+
+	private void updateAuthorizationConsent(OAuth2AuthorizationConsent authorizationConsent) {
+		List<SqlParameterValue> parameters = this.authorizationConsentParametersMapper.apply(authorizationConsent);
+		SqlParameterValue registeredClientId = parameters.remove(0);
+		SqlParameterValue principalName = parameters.remove(0);
+		parameters.add(registeredClientId);
+		parameters.add(principalName);
+		PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
+		this.jdbcOperations.update(UPDATE_AUTHORIZATION_CONSENT_SQL, pss);
+	}
+
+	private void insertAuthorizationConsent(OAuth2AuthorizationConsent authorizationConsent) {
+		List<SqlParameterValue> parameters = this.authorizationConsentParametersMapper.apply(authorizationConsent);
+		PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
+		this.jdbcOperations.update(SAVE_AUTHORIZATION_CONSENT_SQL, pss);
+	}
+
+	@Override
+	public void remove(OAuth2AuthorizationConsent authorizationConsent) {
+		Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
+		SqlParameterValue[] parameters = new SqlParameterValue[] {
+				new SqlParameterValue(Types.VARCHAR, authorizationConsent.getRegisteredClientId()),
+				new SqlParameterValue(Types.VARCHAR, authorizationConsent.getPrincipalName()) };
+		PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters);
+		this.jdbcOperations.update(REMOVE_AUTHORIZATION_CONSENT_SQL, pss);
+	}
+
+	@Override
+	@Nullable
+	public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
+		Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
+		Assert.hasText(principalName, "principalName cannot be empty");
+		SqlParameterValue[] parameters = new SqlParameterValue[] {
+				new SqlParameterValue(Types.VARCHAR, registeredClientId),
+				new SqlParameterValue(Types.VARCHAR, principalName) };
+		PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters);
+		List<OAuth2AuthorizationConsent> result = this.jdbcOperations.query(LOAD_AUTHORIZATION_CONSENT_SQL, pss,
+				this.authorizationConsentRowMapper);
+		return !result.isEmpty() ? result.get(0) : null;
+	}
+
+	/**
+	 * Sets the {@link RowMapper} used for mapping the current row in
+	 * {@code java.sql.ResultSet} to {@link OAuth2AuthorizationConsent}. The default is
+	 * {@link OAuth2AuthorizationConsentRowMapper}.
+	 * @param authorizationConsentRowMapper the {@link RowMapper} used for mapping the
+	 * current row in {@code ResultSet} to {@link OAuth2AuthorizationConsent}
+	 */
+	public final void setAuthorizationConsentRowMapper(
+			RowMapper<OAuth2AuthorizationConsent> authorizationConsentRowMapper) {
+		Assert.notNull(authorizationConsentRowMapper, "authorizationConsentRowMapper cannot be null");
+		this.authorizationConsentRowMapper = authorizationConsentRowMapper;
+	}
+
+	/**
+	 * Sets the {@code Function} used for mapping {@link OAuth2AuthorizationConsent} to a
+	 * {@code List} of {@link SqlParameterValue}. The default is
+	 * {@link OAuth2AuthorizationConsentParametersMapper}.
+	 * @param authorizationConsentParametersMapper the {@code Function} used for mapping
+	 * {@link OAuth2AuthorizationConsent} to a {@code List} of {@link SqlParameterValue}
+	 */
+	public final void setAuthorizationConsentParametersMapper(
+			Function<OAuth2AuthorizationConsent, List<SqlParameterValue>> authorizationConsentParametersMapper) {
+		Assert.notNull(authorizationConsentParametersMapper, "authorizationConsentParametersMapper cannot be null");
+		this.authorizationConsentParametersMapper = authorizationConsentParametersMapper;
+	}
+
+	protected final JdbcOperations getJdbcOperations() {
+		return this.jdbcOperations;
+	}
+
+	protected final RowMapper<OAuth2AuthorizationConsent> getAuthorizationConsentRowMapper() {
+		return this.authorizationConsentRowMapper;
+	}
+
+	protected final Function<OAuth2AuthorizationConsent, List<SqlParameterValue>> getAuthorizationConsentParametersMapper() {
+		return this.authorizationConsentParametersMapper;
+	}
+
+	/**
+	 * The default {@link RowMapper} that maps the current row in {@code ResultSet} to
+	 * {@link OAuth2AuthorizationConsent}.
+	 */
+	public static class OAuth2AuthorizationConsentRowMapper implements RowMapper<OAuth2AuthorizationConsent> {
+
+		private final RegisteredClientRepository registeredClientRepository;
+
+		public OAuth2AuthorizationConsentRowMapper(RegisteredClientRepository registeredClientRepository) {
+			Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+			this.registeredClientRepository = registeredClientRepository;
+		}
+
+		@Override
+		public OAuth2AuthorizationConsent mapRow(ResultSet rs, int rowNum) throws SQLException {
+			String registeredClientId = rs.getString("registered_client_id");
+			RegisteredClient registeredClient = this.registeredClientRepository.findById(registeredClientId);
+			if (registeredClient == null) {
+				throw new DataRetrievalFailureException("The RegisteredClient with id '" + registeredClientId
+						+ "' was not found in the RegisteredClientRepository.");
+			}
+
+			String principalName = rs.getString("principal_name");
+
+			OAuth2AuthorizationConsent.Builder builder = OAuth2AuthorizationConsent.withId(registeredClientId,
+					principalName);
+			String authorizationConsentAuthorities = rs.getString("authorities");
+			if (authorizationConsentAuthorities != null) {
+				for (String authority : StringUtils.commaDelimitedListToSet(authorizationConsentAuthorities)) {
+					builder.authority(new SimpleGrantedAuthority(authority));
+				}
+			}
+			return builder.build();
+		}
+
+		protected final RegisteredClientRepository getRegisteredClientRepository() {
+			return this.registeredClientRepository;
+		}
+
+	}
+
+	/**
+	 * The default {@code Function} that maps {@link OAuth2AuthorizationConsent} to a
+	 * {@code List} of {@link SqlParameterValue}.
+	 */
+	public static class OAuth2AuthorizationConsentParametersMapper
+			implements Function<OAuth2AuthorizationConsent, List<SqlParameterValue>> {
+
+		@Override
+		public List<SqlParameterValue> apply(OAuth2AuthorizationConsent authorizationConsent) {
+			List<SqlParameterValue> parameters = new ArrayList<>();
+			parameters.add(new SqlParameterValue(Types.VARCHAR, authorizationConsent.getRegisteredClientId()));
+			parameters.add(new SqlParameterValue(Types.VARCHAR, authorizationConsent.getPrincipalName()));
+
+			Set<String> authorities = new HashSet<>();
+			for (GrantedAuthority authority : authorizationConsent.getAuthorities()) {
+				authorities.add(authority.getAuthority());
+			}
+			parameters
+				.add(new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToDelimitedString(authorities, ",")));
+			return parameters;
+		}
+
+	}
+
+	static class JdbcOAuth2AuthorizationConsentServiceRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
+
+		@Override
+		public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
+			hints.resources()
+				.registerResource(new ClassPathResource(
+						"org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql"));
+		}
+
+	}
+
+}

+ 853 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationService.java

@@ -0,0 +1,853 @@
+/*
+ * Copyright 2020-2025 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;
+
+import java.nio.charset.StandardCharsets;
+import java.sql.DatabaseMetaData;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.sql.Types;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.RuntimeHintsRegistrar;
+import org.springframework.context.annotation.ImportRuntimeHints;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.dao.DataRetrievalFailureException;
+import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
+import org.springframework.jdbc.core.ConnectionCallback;
+import org.springframework.jdbc.core.JdbcOperations;
+import org.springframework.jdbc.core.PreparedStatementSetter;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.SqlParameterValue;
+import org.springframework.jdbc.support.lob.DefaultLobHandler;
+import org.springframework.jdbc.support.lob.LobCreator;
+import org.springframework.jdbc.support.lob.LobHandler;
+import org.springframework.lang.Nullable;
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2DeviceCode;
+import org.springframework.security.oauth2.core.OAuth2RefreshToken;
+import org.springframework.security.oauth2.core.OAuth2Token;
+import org.springframework.security.oauth2.core.OAuth2UserCode;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * A JDBC implementation of an {@link OAuth2AuthorizationService} that uses a
+ * {@link JdbcOperations} for {@link OAuth2Authorization} persistence.
+ *
+ * <p>
+ * <b>IMPORTANT:</b> This {@code OAuth2AuthorizationService} depends on the table
+ * definition described in
+ * "classpath:org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql"
+ * and therefore MUST be defined in the database schema.
+ *
+ * <p>
+ * <b>NOTE:</b> This {@code OAuth2AuthorizationService} is a simplified JDBC
+ * implementation that MAY be used in a production environment. However, it does have
+ * limitations as it likely won't perform well in an environment requiring high
+ * throughput. The expectation is that the consuming application will provide their own
+ * implementation of {@code OAuth2AuthorizationService} that meets the performance
+ * requirements for its deployment environment.
+ *
+ * @author Ovidiu Popa
+ * @author Joe Grandja
+ * @author Josh Long
+ * @since 0.1.2
+ * @see OAuth2AuthorizationService
+ * @see OAuth2Authorization
+ * @see JdbcOperations
+ * @see RowMapper
+ */
+@ImportRuntimeHints(JdbcOAuth2AuthorizationService.JdbcOAuth2AuthorizationServiceRuntimeHintsRegistrar.class)
+public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationService {
+
+	private static final String REFRESH_TOKEN_VALUE = "refresh_token_value";
+
+	private static final String AUTHORIZATION_CODE_VALUE = "authorization_code_value";
+
+	private static final String ACCESS_TOKEN_VALUE = "access_token_value";
+
+	private static final String OIDC_ID_TOKEN_VALUE = "oidc_id_token_value";
+
+	private static final String USER_CODE_VALUE = "user_code_value";
+
+	private static final String DEVICE_CODE_VALUE = "device_code_value";
+
+	private static final String AUTHORIZATION_CODE_METADATA = "authorization_code_metadata";
+
+	private static final String ACCESS_TOKEN_METADATA = "access_token_metadata";
+
+	private static final String OIDC_ID_TOKEN_METADATA = "oidc_id_token_metadata";
+
+	private static final String REFRESH_TOKEN_METADATA = "refresh_token_metadata";
+
+	private static final String USER_CODE_METADATA = "user_code_metadata";
+
+	private static final String DEVICE_CODE_METADATA = "device_code_metadata";
+
+	// @formatter:off
+	private static final String COLUMN_NAMES = "id, "
+			+ "registered_client_id, "
+			+ "principal_name, "
+			+ "authorization_grant_type, "
+			+ "authorized_scopes, "
+			+ "attributes, "
+			+ "state, "
+			+ "authorization_code_value, "
+			+ "authorization_code_issued_at, "
+			+ "authorization_code_expires_at,"
+			+ "authorization_code_metadata,"
+			+ "access_token_value,"
+			+ "access_token_issued_at,"
+			+ "access_token_expires_at,"
+			+ "access_token_metadata,"
+			+ "access_token_type,"
+			+ "access_token_scopes,"
+			+ "oidc_id_token_value,"
+			+ "oidc_id_token_issued_at,"
+			+ "oidc_id_token_expires_at,"
+			+ "oidc_id_token_metadata,"
+			+ "refresh_token_value,"
+			+ "refresh_token_issued_at,"
+			+ "refresh_token_expires_at,"
+			+ "refresh_token_metadata,"
+			+ "user_code_value,"
+			+ "user_code_issued_at,"
+			+ "user_code_expires_at,"
+			+ "user_code_metadata,"
+			+ "device_code_value,"
+			+ "device_code_issued_at,"
+			+ "device_code_expires_at,"
+			+ "device_code_metadata";
+	// @formatter:on
+
+	private static final String TABLE_NAME = "oauth2_authorization";
+
+	private static final String PK_FILTER = "id = ?";
+
+	private static final String UNKNOWN_TOKEN_TYPE_FILTER = "state = ? OR authorization_code_value = ? OR "
+			+ "access_token_value = ? OR oidc_id_token_value = ? OR refresh_token_value = ? OR user_code_value = ? OR "
+			+ "device_code_value = ?";
+
+	private static final String STATE_FILTER = "state = ?";
+
+	private static final String AUTHORIZATION_CODE_FILTER = "authorization_code_value = ?";
+
+	private static final String ACCESS_TOKEN_FILTER = "access_token_value = ?";
+
+	private static final String ID_TOKEN_FILTER = "oidc_id_token_value = ?";
+
+	private static final String REFRESH_TOKEN_FILTER = "refresh_token_value = ?";
+
+	private static final String USER_CODE_FILTER = "user_code_value = ?";
+
+	private static final String DEVICE_CODE_FILTER = "device_code_value = ?";
+
+	// @formatter:off
+	private static final String LOAD_AUTHORIZATION_SQL = "SELECT " + COLUMN_NAMES
+			+ " FROM " + TABLE_NAME
+			+ " WHERE ";
+	// @formatter:on
+
+	// @formatter:off
+	private static final String SAVE_AUTHORIZATION_SQL = "INSERT INTO " + TABLE_NAME
+			+ " (" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+	// @formatter:on
+
+	// @formatter:off
+	private static final String UPDATE_AUTHORIZATION_SQL = "UPDATE " + TABLE_NAME
+			+ " SET registered_client_id = ?, principal_name = ?, authorization_grant_type = ?, authorized_scopes = ?, attributes = ?, state = ?,"
+			+ " authorization_code_value = ?, authorization_code_issued_at = ?, authorization_code_expires_at = ?, authorization_code_metadata = ?,"
+			+ " access_token_value = ?, access_token_issued_at = ?, access_token_expires_at = ?, access_token_metadata = ?, access_token_type = ?, access_token_scopes = ?,"
+			+ " oidc_id_token_value = ?, oidc_id_token_issued_at = ?, oidc_id_token_expires_at = ?, oidc_id_token_metadata = ?,"
+			+ " refresh_token_value = ?, refresh_token_issued_at = ?, refresh_token_expires_at = ?, refresh_token_metadata = ?,"
+			+ " user_code_value = ?, user_code_issued_at = ?, user_code_expires_at = ?, user_code_metadata = ?,"
+			+ " device_code_value = ?, device_code_issued_at = ?, device_code_expires_at = ?, device_code_metadata = ?"
+			+ " WHERE " + PK_FILTER;
+	// @formatter:on
+
+	private static final String REMOVE_AUTHORIZATION_SQL = "DELETE FROM " + TABLE_NAME + " WHERE " + PK_FILTER;
+
+	private static Map<String, ColumnMetadata> columnMetadataMap;
+
+	private final JdbcOperations jdbcOperations;
+
+	private final LobHandler lobHandler;
+
+	private RowMapper<OAuth2Authorization> authorizationRowMapper;
+
+	private Function<OAuth2Authorization, List<SqlParameterValue>> authorizationParametersMapper;
+
+	/**
+	 * Constructs a {@code JdbcOAuth2AuthorizationService} using the provided parameters.
+	 * @param jdbcOperations the JDBC operations
+	 * @param registeredClientRepository the registered client repository
+	 */
+	public JdbcOAuth2AuthorizationService(JdbcOperations jdbcOperations,
+			RegisteredClientRepository registeredClientRepository) {
+		this(jdbcOperations, registeredClientRepository, new DefaultLobHandler());
+	}
+
+	/**
+	 * Constructs a {@code JdbcOAuth2AuthorizationService} using the provided parameters.
+	 * @param jdbcOperations the JDBC operations
+	 * @param registeredClientRepository the registered client repository
+	 * @param lobHandler the handler for large binary fields and large text fields
+	 */
+	public JdbcOAuth2AuthorizationService(JdbcOperations jdbcOperations,
+			RegisteredClientRepository registeredClientRepository, LobHandler lobHandler) {
+		Assert.notNull(jdbcOperations, "jdbcOperations cannot be null");
+		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+		Assert.notNull(lobHandler, "lobHandler cannot be null");
+		this.jdbcOperations = jdbcOperations;
+		this.lobHandler = lobHandler;
+		OAuth2AuthorizationRowMapper authorizationRowMapper = new OAuth2AuthorizationRowMapper(
+				registeredClientRepository);
+		authorizationRowMapper.setLobHandler(lobHandler);
+		this.authorizationRowMapper = authorizationRowMapper;
+		this.authorizationParametersMapper = new OAuth2AuthorizationParametersMapper();
+		initColumnMetadata(jdbcOperations);
+	}
+
+	@Override
+	public void save(OAuth2Authorization authorization) {
+		Assert.notNull(authorization, "authorization cannot be null");
+		OAuth2Authorization existingAuthorization = findById(authorization.getId());
+		if (existingAuthorization == null) {
+			insertAuthorization(authorization);
+		}
+		else {
+			updateAuthorization(authorization);
+		}
+	}
+
+	private void updateAuthorization(OAuth2Authorization authorization) {
+		List<SqlParameterValue> parameters = this.authorizationParametersMapper.apply(authorization);
+		SqlParameterValue id = parameters.remove(0);
+		parameters.add(id);
+		try (LobCreator lobCreator = this.lobHandler.getLobCreator()) {
+			PreparedStatementSetter pss = new LobCreatorArgumentPreparedStatementSetter(lobCreator,
+					parameters.toArray());
+			this.jdbcOperations.update(UPDATE_AUTHORIZATION_SQL, pss);
+		}
+	}
+
+	private void insertAuthorization(OAuth2Authorization authorization) {
+		List<SqlParameterValue> parameters = this.authorizationParametersMapper.apply(authorization);
+		try (LobCreator lobCreator = this.lobHandler.getLobCreator()) {
+			PreparedStatementSetter pss = new LobCreatorArgumentPreparedStatementSetter(lobCreator,
+					parameters.toArray());
+			this.jdbcOperations.update(SAVE_AUTHORIZATION_SQL, pss);
+		}
+	}
+
+	@Override
+	public void remove(OAuth2Authorization authorization) {
+		Assert.notNull(authorization, "authorization cannot be null");
+		SqlParameterValue[] parameters = new SqlParameterValue[] {
+				new SqlParameterValue(Types.VARCHAR, authorization.getId()) };
+		PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters);
+		this.jdbcOperations.update(REMOVE_AUTHORIZATION_SQL, pss);
+	}
+
+	@Nullable
+	@Override
+	public OAuth2Authorization findById(String id) {
+		Assert.hasText(id, "id cannot be empty");
+		List<SqlParameterValue> parameters = new ArrayList<>();
+		parameters.add(new SqlParameterValue(Types.VARCHAR, id));
+		return findBy(PK_FILTER, parameters);
+	}
+
+	@Nullable
+	@Override
+	public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
+		Assert.hasText(token, "token cannot be empty");
+		List<SqlParameterValue> parameters = new ArrayList<>();
+		if (tokenType == null) {
+			parameters.add(new SqlParameterValue(Types.VARCHAR, token));
+			parameters.add(mapToSqlParameter(AUTHORIZATION_CODE_VALUE, token));
+			parameters.add(mapToSqlParameter(ACCESS_TOKEN_VALUE, token));
+			parameters.add(mapToSqlParameter(OIDC_ID_TOKEN_VALUE, token));
+			parameters.add(mapToSqlParameter(REFRESH_TOKEN_VALUE, token));
+			parameters.add(mapToSqlParameter(USER_CODE_VALUE, token));
+			parameters.add(mapToSqlParameter(DEVICE_CODE_VALUE, token));
+			return findBy(UNKNOWN_TOKEN_TYPE_FILTER, parameters);
+		}
+		else if (OAuth2ParameterNames.STATE.equals(tokenType.getValue())) {
+			parameters.add(new SqlParameterValue(Types.VARCHAR, token));
+			return findBy(STATE_FILTER, parameters);
+		}
+		else if (OAuth2ParameterNames.CODE.equals(tokenType.getValue())) {
+			parameters.add(mapToSqlParameter(AUTHORIZATION_CODE_VALUE, token));
+			return findBy(AUTHORIZATION_CODE_FILTER, parameters);
+		}
+		else if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenType)) {
+			parameters.add(mapToSqlParameter(ACCESS_TOKEN_VALUE, token));
+			return findBy(ACCESS_TOKEN_FILTER, parameters);
+		}
+		else if (OidcParameterNames.ID_TOKEN.equals(tokenType.getValue())) {
+			parameters.add(mapToSqlParameter(OIDC_ID_TOKEN_VALUE, token));
+			return findBy(ID_TOKEN_FILTER, parameters);
+		}
+		else if (OAuth2TokenType.REFRESH_TOKEN.equals(tokenType)) {
+			parameters.add(mapToSqlParameter(REFRESH_TOKEN_VALUE, token));
+			return findBy(REFRESH_TOKEN_FILTER, parameters);
+		}
+		else if (OAuth2ParameterNames.USER_CODE.equals(tokenType.getValue())) {
+			parameters.add(mapToSqlParameter(USER_CODE_VALUE, token));
+			return findBy(USER_CODE_FILTER, parameters);
+		}
+		else if (OAuth2ParameterNames.DEVICE_CODE.equals(tokenType.getValue())) {
+			parameters.add(mapToSqlParameter(DEVICE_CODE_VALUE, token));
+			return findBy(DEVICE_CODE_FILTER, parameters);
+		}
+		return null;
+	}
+
+	private OAuth2Authorization findBy(String filter, List<SqlParameterValue> parameters) {
+		try (LobCreator lobCreator = getLobHandler().getLobCreator()) {
+			PreparedStatementSetter pss = new LobCreatorArgumentPreparedStatementSetter(lobCreator,
+					parameters.toArray());
+			List<OAuth2Authorization> result = getJdbcOperations().query(LOAD_AUTHORIZATION_SQL + filter, pss,
+					getAuthorizationRowMapper());
+			return !result.isEmpty() ? result.get(0) : null;
+		}
+	}
+
+	/**
+	 * Sets the {@link RowMapper} used for mapping the current row in
+	 * {@code java.sql.ResultSet} to {@link OAuth2Authorization}. The default is
+	 * {@link OAuth2AuthorizationRowMapper}.
+	 * @param authorizationRowMapper the {@link RowMapper} used for mapping the current
+	 * row in {@code ResultSet} to {@link OAuth2Authorization}
+	 */
+	public final void setAuthorizationRowMapper(RowMapper<OAuth2Authorization> authorizationRowMapper) {
+		Assert.notNull(authorizationRowMapper, "authorizationRowMapper cannot be null");
+		this.authorizationRowMapper = authorizationRowMapper;
+	}
+
+	/**
+	 * Sets the {@code Function} used for mapping {@link OAuth2Authorization} to a
+	 * {@code List} of {@link SqlParameterValue}. The default is
+	 * {@link OAuth2AuthorizationParametersMapper}.
+	 * @param authorizationParametersMapper the {@code Function} used for mapping
+	 * {@link OAuth2Authorization} to a {@code List} of {@link SqlParameterValue}
+	 */
+	public final void setAuthorizationParametersMapper(
+			Function<OAuth2Authorization, List<SqlParameterValue>> authorizationParametersMapper) {
+		Assert.notNull(authorizationParametersMapper, "authorizationParametersMapper cannot be null");
+		this.authorizationParametersMapper = authorizationParametersMapper;
+	}
+
+	protected final JdbcOperations getJdbcOperations() {
+		return this.jdbcOperations;
+	}
+
+	protected final LobHandler getLobHandler() {
+		return this.lobHandler;
+	}
+
+	protected final RowMapper<OAuth2Authorization> getAuthorizationRowMapper() {
+		return this.authorizationRowMapper;
+	}
+
+	protected final Function<OAuth2Authorization, List<SqlParameterValue>> getAuthorizationParametersMapper() {
+		return this.authorizationParametersMapper;
+	}
+
+	private static void initColumnMetadata(JdbcOperations jdbcOperations) {
+		columnMetadataMap = new HashMap<>();
+		ColumnMetadata columnMetadata;
+
+		columnMetadata = getColumnMetadata(jdbcOperations, "attributes", Types.BLOB);
+		columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
+		columnMetadata = getColumnMetadata(jdbcOperations, AUTHORIZATION_CODE_VALUE, Types.BLOB);
+		columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
+		columnMetadata = getColumnMetadata(jdbcOperations, AUTHORIZATION_CODE_METADATA, Types.BLOB);
+		columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
+		columnMetadata = getColumnMetadata(jdbcOperations, ACCESS_TOKEN_VALUE, Types.BLOB);
+		columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
+		columnMetadata = getColumnMetadata(jdbcOperations, ACCESS_TOKEN_METADATA, Types.BLOB);
+		columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
+		columnMetadata = getColumnMetadata(jdbcOperations, OIDC_ID_TOKEN_VALUE, Types.BLOB);
+		columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
+		columnMetadata = getColumnMetadata(jdbcOperations, OIDC_ID_TOKEN_METADATA, Types.BLOB);
+		columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
+		columnMetadata = getColumnMetadata(jdbcOperations, REFRESH_TOKEN_VALUE, Types.BLOB);
+		columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
+		columnMetadata = getColumnMetadata(jdbcOperations, REFRESH_TOKEN_METADATA, Types.BLOB);
+		columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
+		columnMetadata = getColumnMetadata(jdbcOperations, USER_CODE_VALUE, Types.BLOB);
+		columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
+		columnMetadata = getColumnMetadata(jdbcOperations, USER_CODE_METADATA, Types.BLOB);
+		columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
+		columnMetadata = getColumnMetadata(jdbcOperations, DEVICE_CODE_VALUE, Types.BLOB);
+		columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
+		columnMetadata = getColumnMetadata(jdbcOperations, DEVICE_CODE_METADATA, Types.BLOB);
+		columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
+	}
+
+	private static ColumnMetadata getColumnMetadata(JdbcOperations jdbcOperations, String columnName,
+			int defaultDataType) {
+		Integer dataType = jdbcOperations.execute((ConnectionCallback<Integer>) (conn) -> {
+			DatabaseMetaData databaseMetaData = conn.getMetaData();
+			ResultSet rs = databaseMetaData.getColumns(null, null, TABLE_NAME, columnName);
+			if (rs.next()) {
+				return rs.getInt("DATA_TYPE");
+			}
+			// NOTE: (Applies to HSQL)
+			// When a database object is created with one of the CREATE statements or
+			// renamed with the ALTER statement,
+			// if the name is enclosed in double quotes, the exact name is used as the
+			// case-normal form.
+			// But if it is not enclosed in double quotes,
+			// the name is converted to uppercase and this uppercase version is stored in
+			// the database as the case-normal form.
+			rs = databaseMetaData.getColumns(null, null, TABLE_NAME.toUpperCase(Locale.ENGLISH),
+					columnName.toUpperCase(Locale.ENGLISH));
+			if (rs.next()) {
+				return rs.getInt("DATA_TYPE");
+			}
+			return null;
+		});
+		return new ColumnMetadata(columnName, (dataType != null) ? dataType : defaultDataType);
+	}
+
+	private static SqlParameterValue mapToSqlParameter(String columnName, String value) {
+		ColumnMetadata columnMetadata = columnMetadataMap.get(columnName);
+		return (Types.BLOB == columnMetadata.getDataType() && StringUtils.hasText(value))
+				? new SqlParameterValue(Types.BLOB, value.getBytes(StandardCharsets.UTF_8))
+				: new SqlParameterValue(columnMetadata.getDataType(), value);
+	}
+
+	/**
+	 * The default {@link RowMapper} that maps the current row in
+	 * {@code java.sql.ResultSet} to {@link OAuth2Authorization}.
+	 */
+	public static class OAuth2AuthorizationRowMapper implements RowMapper<OAuth2Authorization> {
+
+		private final RegisteredClientRepository registeredClientRepository;
+
+		private LobHandler lobHandler = new DefaultLobHandler();
+
+		private ObjectMapper objectMapper = new ObjectMapper();
+
+		public OAuth2AuthorizationRowMapper(RegisteredClientRepository registeredClientRepository) {
+			Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+			this.registeredClientRepository = registeredClientRepository;
+
+			ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();
+			List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
+			this.objectMapper.registerModules(securityModules);
+			this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
+		}
+
+		@Override
+		@SuppressWarnings("unchecked")
+		public OAuth2Authorization mapRow(ResultSet rs, int rowNum) throws SQLException {
+			String registeredClientId = rs.getString("registered_client_id");
+			RegisteredClient registeredClient = this.registeredClientRepository.findById(registeredClientId);
+			if (registeredClient == null) {
+				throw new DataRetrievalFailureException("The RegisteredClient with id '" + registeredClientId
+						+ "' was not found in the RegisteredClientRepository.");
+			}
+
+			OAuth2Authorization.Builder builder = OAuth2Authorization.withRegisteredClient(registeredClient);
+			String id = rs.getString("id");
+			String principalName = rs.getString("principal_name");
+			String authorizationGrantType = rs.getString("authorization_grant_type");
+			Set<String> authorizedScopes = Collections.emptySet();
+			String authorizedScopesString = rs.getString("authorized_scopes");
+			if (authorizedScopesString != null) {
+				authorizedScopes = StringUtils.commaDelimitedListToSet(authorizedScopesString);
+			}
+			Map<String, Object> attributes = parseMap(getLobValue(rs, "attributes"));
+
+			builder.id(id)
+				.principalName(principalName)
+				.authorizationGrantType(new AuthorizationGrantType(authorizationGrantType))
+				.authorizedScopes(authorizedScopes)
+				.attributes((attrs) -> attrs.putAll(attributes));
+
+			String state = rs.getString("state");
+			if (StringUtils.hasText(state)) {
+				builder.attribute(OAuth2ParameterNames.STATE, state);
+			}
+
+			Instant tokenIssuedAt;
+			Instant tokenExpiresAt;
+			String authorizationCodeValue = getLobValue(rs, AUTHORIZATION_CODE_VALUE);
+
+			if (StringUtils.hasText(authorizationCodeValue)) {
+				tokenIssuedAt = rs.getTimestamp("authorization_code_issued_at").toInstant();
+				tokenExpiresAt = rs.getTimestamp("authorization_code_expires_at").toInstant();
+				Map<String, Object> authorizationCodeMetadata = parseMap(getLobValue(rs, AUTHORIZATION_CODE_METADATA));
+
+				OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode(authorizationCodeValue,
+						tokenIssuedAt, tokenExpiresAt);
+				builder.token(authorizationCode, (metadata) -> metadata.putAll(authorizationCodeMetadata));
+			}
+
+			String accessTokenValue = getLobValue(rs, ACCESS_TOKEN_VALUE);
+			if (StringUtils.hasText(accessTokenValue)) {
+				tokenIssuedAt = rs.getTimestamp("access_token_issued_at").toInstant();
+				tokenExpiresAt = rs.getTimestamp("access_token_expires_at").toInstant();
+				Map<String, Object> accessTokenMetadata = parseMap(getLobValue(rs, ACCESS_TOKEN_METADATA));
+				OAuth2AccessToken.TokenType tokenType = null;
+				if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(rs.getString("access_token_type"))) {
+					tokenType = OAuth2AccessToken.TokenType.BEARER;
+				}
+				else if (OAuth2AccessToken.TokenType.DPOP.getValue()
+					.equalsIgnoreCase(rs.getString("access_token_type"))) {
+					tokenType = OAuth2AccessToken.TokenType.DPOP;
+				}
+
+				Set<String> scopes = Collections.emptySet();
+				String accessTokenScopes = rs.getString("access_token_scopes");
+				if (accessTokenScopes != null) {
+					scopes = StringUtils.commaDelimitedListToSet(accessTokenScopes);
+				}
+				OAuth2AccessToken accessToken = new OAuth2AccessToken(tokenType, accessTokenValue, tokenIssuedAt,
+						tokenExpiresAt, scopes);
+				builder.token(accessToken, (metadata) -> metadata.putAll(accessTokenMetadata));
+			}
+
+			String oidcIdTokenValue = getLobValue(rs, OIDC_ID_TOKEN_VALUE);
+			if (StringUtils.hasText(oidcIdTokenValue)) {
+				tokenIssuedAt = rs.getTimestamp("oidc_id_token_issued_at").toInstant();
+				tokenExpiresAt = rs.getTimestamp("oidc_id_token_expires_at").toInstant();
+				Map<String, Object> oidcTokenMetadata = parseMap(getLobValue(rs, OIDC_ID_TOKEN_METADATA));
+
+				OidcIdToken oidcToken = new OidcIdToken(oidcIdTokenValue, tokenIssuedAt, tokenExpiresAt,
+						(Map<String, Object>) oidcTokenMetadata.get(OAuth2Authorization.Token.CLAIMS_METADATA_NAME));
+				builder.token(oidcToken, (metadata) -> metadata.putAll(oidcTokenMetadata));
+			}
+
+			String refreshTokenValue = getLobValue(rs, REFRESH_TOKEN_VALUE);
+			if (StringUtils.hasText(refreshTokenValue)) {
+				tokenIssuedAt = rs.getTimestamp("refresh_token_issued_at").toInstant();
+				tokenExpiresAt = null;
+				Timestamp refreshTokenExpiresAt = rs.getTimestamp("refresh_token_expires_at");
+				if (refreshTokenExpiresAt != null) {
+					tokenExpiresAt = refreshTokenExpiresAt.toInstant();
+				}
+				Map<String, Object> refreshTokenMetadata = parseMap(getLobValue(rs, REFRESH_TOKEN_METADATA));
+
+				OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(refreshTokenValue, tokenIssuedAt,
+						tokenExpiresAt);
+				builder.token(refreshToken, (metadata) -> metadata.putAll(refreshTokenMetadata));
+			}
+
+			String userCodeValue = getLobValue(rs, USER_CODE_VALUE);
+			if (StringUtils.hasText(userCodeValue)) {
+				tokenIssuedAt = rs.getTimestamp("user_code_issued_at").toInstant();
+				tokenExpiresAt = rs.getTimestamp("user_code_expires_at").toInstant();
+				Map<String, Object> userCodeMetadata = parseMap(getLobValue(rs, USER_CODE_METADATA));
+
+				OAuth2UserCode userCode = new OAuth2UserCode(userCodeValue, tokenIssuedAt, tokenExpiresAt);
+				builder.token(userCode, (metadata) -> metadata.putAll(userCodeMetadata));
+			}
+
+			String deviceCodeValue = getLobValue(rs, DEVICE_CODE_VALUE);
+			if (StringUtils.hasText(deviceCodeValue)) {
+				tokenIssuedAt = rs.getTimestamp("device_code_issued_at").toInstant();
+				tokenExpiresAt = rs.getTimestamp("device_code_expires_at").toInstant();
+				Map<String, Object> deviceCodeMetadata = parseMap(getLobValue(rs, DEVICE_CODE_METADATA));
+
+				OAuth2DeviceCode deviceCode = new OAuth2DeviceCode(deviceCodeValue, tokenIssuedAt, tokenExpiresAt);
+				builder.token(deviceCode, (metadata) -> metadata.putAll(deviceCodeMetadata));
+			}
+
+			return builder.build();
+		}
+
+		private String getLobValue(ResultSet rs, String columnName) throws SQLException {
+			String columnValue = null;
+			ColumnMetadata columnMetadata = columnMetadataMap.get(columnName);
+			if (Types.BLOB == columnMetadata.getDataType()) {
+				byte[] columnValueBytes = this.lobHandler.getBlobAsBytes(rs, columnName);
+				if (columnValueBytes != null) {
+					columnValue = new String(columnValueBytes, StandardCharsets.UTF_8);
+				}
+			}
+			else if (Types.CLOB == columnMetadata.getDataType()) {
+				columnValue = this.lobHandler.getClobAsString(rs, columnName);
+			}
+			else {
+				columnValue = rs.getString(columnName);
+			}
+			return columnValue;
+		}
+
+		public final void setLobHandler(LobHandler lobHandler) {
+			Assert.notNull(lobHandler, "lobHandler cannot be null");
+			this.lobHandler = lobHandler;
+		}
+
+		public final void setObjectMapper(ObjectMapper objectMapper) {
+			Assert.notNull(objectMapper, "objectMapper cannot be null");
+			this.objectMapper = objectMapper;
+		}
+
+		protected final RegisteredClientRepository getRegisteredClientRepository() {
+			return this.registeredClientRepository;
+		}
+
+		protected final LobHandler getLobHandler() {
+			return this.lobHandler;
+		}
+
+		protected final ObjectMapper getObjectMapper() {
+			return this.objectMapper;
+		}
+
+		private Map<String, Object> parseMap(String data) {
+			try {
+				return this.objectMapper.readValue(data, new TypeReference<>() {
+				});
+			}
+			catch (Exception ex) {
+				throw new IllegalArgumentException(ex.getMessage(), ex);
+			}
+		}
+
+	}
+
+	/**
+	 * The default {@code Function} that maps {@link OAuth2Authorization} to a
+	 * {@code List} of {@link SqlParameterValue}.
+	 */
+	public static class OAuth2AuthorizationParametersMapper
+			implements Function<OAuth2Authorization, List<SqlParameterValue>> {
+
+		private ObjectMapper objectMapper = new ObjectMapper();
+
+		public OAuth2AuthorizationParametersMapper() {
+			ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();
+			List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
+			this.objectMapper.registerModules(securityModules);
+			this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
+		}
+
+		@Override
+		public List<SqlParameterValue> apply(OAuth2Authorization authorization) {
+			List<SqlParameterValue> parameters = new ArrayList<>();
+			parameters.add(new SqlParameterValue(Types.VARCHAR, authorization.getId()));
+			parameters.add(new SqlParameterValue(Types.VARCHAR, authorization.getRegisteredClientId()));
+			parameters.add(new SqlParameterValue(Types.VARCHAR, authorization.getPrincipalName()));
+			parameters.add(new SqlParameterValue(Types.VARCHAR, authorization.getAuthorizationGrantType().getValue()));
+
+			String authorizedScopes = null;
+			if (!CollectionUtils.isEmpty(authorization.getAuthorizedScopes())) {
+				authorizedScopes = StringUtils.collectionToDelimitedString(authorization.getAuthorizedScopes(), ",");
+			}
+			parameters.add(new SqlParameterValue(Types.VARCHAR, authorizedScopes));
+
+			String attributes = writeMap(authorization.getAttributes());
+			parameters.add(mapToSqlParameter("attributes", attributes));
+
+			String state = null;
+			String authorizationState = authorization.getAttribute(OAuth2ParameterNames.STATE);
+			if (StringUtils.hasText(authorizationState)) {
+				state = authorizationState;
+			}
+			parameters.add(new SqlParameterValue(Types.VARCHAR, state));
+
+			OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
+				.getToken(OAuth2AuthorizationCode.class);
+			List<SqlParameterValue> authorizationCodeSqlParameters = toSqlParameterList(AUTHORIZATION_CODE_VALUE,
+					AUTHORIZATION_CODE_METADATA, authorizationCode);
+			parameters.addAll(authorizationCodeSqlParameters);
+
+			OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getToken(OAuth2AccessToken.class);
+			List<SqlParameterValue> accessTokenSqlParameters = toSqlParameterList(ACCESS_TOKEN_VALUE,
+					ACCESS_TOKEN_METADATA, accessToken);
+			parameters.addAll(accessTokenSqlParameters);
+			String accessTokenType = null;
+			String accessTokenScopes = null;
+			if (accessToken != null) {
+				accessTokenType = accessToken.getToken().getTokenType().getValue();
+				if (!CollectionUtils.isEmpty(accessToken.getToken().getScopes())) {
+					accessTokenScopes = StringUtils.collectionToDelimitedString(accessToken.getToken().getScopes(),
+							",");
+				}
+			}
+			parameters.add(new SqlParameterValue(Types.VARCHAR, accessTokenType));
+			parameters.add(new SqlParameterValue(Types.VARCHAR, accessTokenScopes));
+
+			OAuth2Authorization.Token<OidcIdToken> oidcIdToken = authorization.getToken(OidcIdToken.class);
+			List<SqlParameterValue> oidcIdTokenSqlParameters = toSqlParameterList(OIDC_ID_TOKEN_VALUE,
+					OIDC_ID_TOKEN_METADATA, oidcIdToken);
+			parameters.addAll(oidcIdTokenSqlParameters);
+
+			OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken = authorization.getRefreshToken();
+			List<SqlParameterValue> refreshTokenSqlParameters = toSqlParameterList(REFRESH_TOKEN_VALUE,
+					REFRESH_TOKEN_METADATA, refreshToken);
+			parameters.addAll(refreshTokenSqlParameters);
+
+			OAuth2Authorization.Token<OAuth2UserCode> userCode = authorization.getToken(OAuth2UserCode.class);
+			List<SqlParameterValue> userCodeSqlParameters = toSqlParameterList(USER_CODE_VALUE, USER_CODE_METADATA,
+					userCode);
+			parameters.addAll(userCodeSqlParameters);
+
+			OAuth2Authorization.Token<OAuth2DeviceCode> deviceCode = authorization.getToken(OAuth2DeviceCode.class);
+			List<SqlParameterValue> deviceCodeSqlParameters = toSqlParameterList(DEVICE_CODE_VALUE,
+					DEVICE_CODE_METADATA, deviceCode);
+			parameters.addAll(deviceCodeSqlParameters);
+
+			return parameters;
+		}
+
+		public final void setObjectMapper(ObjectMapper objectMapper) {
+			Assert.notNull(objectMapper, "objectMapper cannot be null");
+			this.objectMapper = objectMapper;
+		}
+
+		protected final ObjectMapper getObjectMapper() {
+			return this.objectMapper;
+		}
+
+		private <T extends OAuth2Token> List<SqlParameterValue> toSqlParameterList(String tokenColumnName,
+				String tokenMetadataColumnName, OAuth2Authorization.Token<T> token) {
+
+			List<SqlParameterValue> parameters = new ArrayList<>();
+			String tokenValue = null;
+			Timestamp tokenIssuedAt = null;
+			Timestamp tokenExpiresAt = null;
+			String metadata = null;
+			if (token != null) {
+				tokenValue = token.getToken().getTokenValue();
+				if (token.getToken().getIssuedAt() != null) {
+					tokenIssuedAt = Timestamp.from(token.getToken().getIssuedAt());
+				}
+				if (token.getToken().getExpiresAt() != null) {
+					tokenExpiresAt = Timestamp.from(token.getToken().getExpiresAt());
+				}
+				metadata = writeMap(token.getMetadata());
+			}
+
+			parameters.add(mapToSqlParameter(tokenColumnName, tokenValue));
+			parameters.add(new SqlParameterValue(Types.TIMESTAMP, tokenIssuedAt));
+			parameters.add(new SqlParameterValue(Types.TIMESTAMP, tokenExpiresAt));
+			parameters.add(mapToSqlParameter(tokenMetadataColumnName, metadata));
+			return parameters;
+		}
+
+		private String writeMap(Map<String, Object> data) {
+			try {
+				return this.objectMapper.writeValueAsString(data);
+			}
+			catch (Exception ex) {
+				throw new IllegalArgumentException(ex.getMessage(), ex);
+			}
+		}
+
+	}
+
+	private static final class LobCreatorArgumentPreparedStatementSetter extends ArgumentPreparedStatementSetter {
+
+		private final LobCreator lobCreator;
+
+		private LobCreatorArgumentPreparedStatementSetter(LobCreator lobCreator, Object[] args) {
+			super(args);
+			this.lobCreator = lobCreator;
+		}
+
+		@Override
+		protected void doSetValue(PreparedStatement ps, int parameterPosition, Object argValue) throws SQLException {
+			if (argValue instanceof SqlParameterValue paramValue) {
+				if (paramValue.getSqlType() == Types.BLOB) {
+					if (paramValue.getValue() != null) {
+						Assert.isInstanceOf(byte[].class, paramValue.getValue(),
+								"Value of blob parameter must be byte[]");
+					}
+					byte[] valueBytes = (byte[]) paramValue.getValue();
+					this.lobCreator.setBlobAsBytes(ps, parameterPosition, valueBytes);
+					return;
+				}
+				if (paramValue.getSqlType() == Types.CLOB) {
+					if (paramValue.getValue() != null) {
+						Assert.isInstanceOf(String.class, paramValue.getValue(),
+								"Value of clob parameter must be String");
+					}
+					String valueString = (String) paramValue.getValue();
+					this.lobCreator.setClobAsString(ps, parameterPosition, valueString);
+					return;
+				}
+			}
+			super.doSetValue(ps, parameterPosition, argValue);
+		}
+
+	}
+
+	private static final class ColumnMetadata {
+
+		private final String columnName;
+
+		private final int dataType;
+
+		private ColumnMetadata(String columnName, int dataType) {
+			this.columnName = columnName;
+			this.dataType = dataType;
+		}
+
+		private String getColumnName() {
+			return this.columnName;
+		}
+
+		private int getDataType() {
+			return this.dataType;
+		}
+
+	}
+
+	static class JdbcOAuth2AuthorizationServiceRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
+
+		@Override
+		public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
+			hints.resources()
+				.registerResource(new ClassPathResource(
+						"org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql"));
+		}
+
+	}
+
+}

+ 575 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2Authorization.java

@@ -0,0 +1,575 @@
+/*
+ * Copyright 2020-2025 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;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Consumer;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2RefreshToken;
+import org.springframework.security.oauth2.core.OAuth2Token;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * A representation of an OAuth 2.0 Authorization, which holds state related to the
+ * authorization granted to a {@link #getRegisteredClientId() client}, by the
+ * {@link #getPrincipalName() resource owner} or itself in the case of the
+ * {@code client_credentials} grant type.
+ *
+ * @author Joe Grandja
+ * @author Krisztian Toth
+ * @since 0.0.1
+ * @see RegisteredClient
+ * @see AuthorizationGrantType
+ * @see OAuth2Token
+ * @see OAuth2AccessToken
+ * @see OAuth2RefreshToken
+ */
+public class OAuth2Authorization implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 880363144799377926L;
+
+	private String id;
+
+	private String registeredClientId;
+
+	private String principalName;
+
+	private AuthorizationGrantType authorizationGrantType;
+
+	private Set<String> authorizedScopes;
+
+	private Map<Class<? extends OAuth2Token>, Token<?>> tokens;
+
+	private Map<String, Object> attributes;
+
+	protected OAuth2Authorization() {
+	}
+
+	/**
+	 * Returns the identifier for the authorization.
+	 * @return the identifier for the authorization
+	 */
+	public String getId() {
+		return this.id;
+	}
+
+	/**
+	 * Returns the identifier for the {@link RegisteredClient#getId() registered client}.
+	 * @return the {@link RegisteredClient#getId()}
+	 */
+	public String getRegisteredClientId() {
+		return this.registeredClientId;
+	}
+
+	/**
+	 * Returns the {@code Principal} name of the resource owner (or client).
+	 * @return the {@code Principal} name of the resource owner (or client)
+	 */
+	public String getPrincipalName() {
+		return this.principalName;
+	}
+
+	/**
+	 * Returns the {@link AuthorizationGrantType authorization grant type} used for the
+	 * authorization.
+	 * @return the {@link AuthorizationGrantType} used for the authorization
+	 */
+	public AuthorizationGrantType getAuthorizationGrantType() {
+		return this.authorizationGrantType;
+	}
+
+	/**
+	 * Returns the authorized scope(s).
+	 * @return the {@code Set} of authorized scope(s)
+	 * @since 0.4.0
+	 */
+	public Set<String> getAuthorizedScopes() {
+		return this.authorizedScopes;
+	}
+
+	/**
+	 * Returns the {@link Token} of type {@link OAuth2AccessToken}.
+	 * @return the {@link Token} of type {@link OAuth2AccessToken}
+	 */
+	public Token<OAuth2AccessToken> getAccessToken() {
+		return getToken(OAuth2AccessToken.class);
+	}
+
+	/**
+	 * Returns the {@link Token} of type {@link OAuth2RefreshToken}.
+	 * @return the {@link Token} of type {@link OAuth2RefreshToken}, or {@code null} if
+	 * not available
+	 */
+	@Nullable
+	public Token<OAuth2RefreshToken> getRefreshToken() {
+		return getToken(OAuth2RefreshToken.class);
+	}
+
+	/**
+	 * Returns the {@link Token} of type {@code tokenType}.
+	 * @param tokenType the token type
+	 * @param <T> the type of the token
+	 * @return the {@link Token}, or {@code null} if not available
+	 */
+	@Nullable
+	@SuppressWarnings("unchecked")
+	public <T extends OAuth2Token> Token<T> getToken(Class<T> tokenType) {
+		Assert.notNull(tokenType, "tokenType cannot be null");
+		Token<?> token = this.tokens.get(tokenType);
+		return (token != null) ? (Token<T>) token : null;
+	}
+
+	/**
+	 * Returns the {@link Token} matching the {@code tokenValue}.
+	 * @param tokenValue the token value
+	 * @param <T> the type of the token
+	 * @return the {@link Token}, or {@code null} if not available
+	 */
+	@Nullable
+	@SuppressWarnings("unchecked")
+	public <T extends OAuth2Token> Token<T> getToken(String tokenValue) {
+		Assert.hasText(tokenValue, "tokenValue cannot be empty");
+		for (Token<?> token : this.tokens.values()) {
+			if (token.getToken().getTokenValue().equals(tokenValue)) {
+				return (Token<T>) token;
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Returns the attribute(s) associated to the authorization.
+	 * @return a {@code Map} of the attribute(s)
+	 */
+	public Map<String, Object> getAttributes() {
+		return this.attributes;
+	}
+
+	/**
+	 * Returns the value of an attribute associated to the authorization.
+	 * @param name the name of the attribute
+	 * @param <T> the type of the attribute
+	 * @return the value of an attribute associated to the authorization, or {@code null}
+	 * if not available
+	 */
+	@Nullable
+	@SuppressWarnings("unchecked")
+	public <T> T getAttribute(String name) {
+		Assert.hasText(name, "name cannot be empty");
+		return (T) this.attributes.get(name);
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (obj == null || getClass() != obj.getClass()) {
+			return false;
+		}
+		OAuth2Authorization that = (OAuth2Authorization) obj;
+		return Objects.equals(this.id, that.id) && Objects.equals(this.registeredClientId, that.registeredClientId)
+				&& Objects.equals(this.principalName, that.principalName)
+				&& Objects.equals(this.authorizationGrantType, that.authorizationGrantType)
+				&& Objects.equals(this.authorizedScopes, that.authorizedScopes)
+				&& Objects.equals(this.tokens, that.tokens) && Objects.equals(this.attributes, that.attributes);
+	}
+
+	@Override
+	public int hashCode() {
+		return Objects.hash(this.id, this.registeredClientId, this.principalName, this.authorizationGrantType,
+				this.authorizedScopes, this.tokens, this.attributes);
+	}
+
+	/**
+	 * Returns a new {@link Builder}, initialized with the provided
+	 * {@link RegisteredClient#getId()}.
+	 * @param registeredClient the {@link RegisteredClient}
+	 * @return the {@link Builder}
+	 */
+	public static Builder withRegisteredClient(RegisteredClient registeredClient) {
+		Assert.notNull(registeredClient, "registeredClient cannot be null");
+		return new Builder(registeredClient.getId());
+	}
+
+	/**
+	 * Returns a new {@link Builder}, initialized with the values from the provided
+	 * {@code OAuth2Authorization}.
+	 * @param authorization the {@code OAuth2Authorization} used for initializing the
+	 * {@link Builder}
+	 * @return the {@link Builder}
+	 */
+	public static Builder from(OAuth2Authorization authorization) {
+		Assert.notNull(authorization, "authorization cannot be null");
+		return new Builder(authorization.getRegisteredClientId()).id(authorization.getId())
+			.principalName(authorization.getPrincipalName())
+			.authorizationGrantType(authorization.getAuthorizationGrantType())
+			.authorizedScopes(authorization.getAuthorizedScopes())
+			.tokens(authorization.tokens)
+			.attributes((attrs) -> attrs.putAll(authorization.getAttributes()));
+	}
+
+	/**
+	 * A holder of an OAuth 2.0 Token and it's associated metadata.
+	 *
+	 * @param <T> the type of the {@link OAuth2Token}
+	 * @author Joe Grandja
+	 * @since 0.1.0
+	 */
+	public static class Token<T extends OAuth2Token> implements Serializable {
+
+		@Serial
+		private static final long serialVersionUID = -5931125502413497522L;
+
+		protected static final String TOKEN_METADATA_NAMESPACE = "metadata.token.";
+
+		/**
+		 * The name of the metadata that indicates if the token has been invalidated.
+		 */
+		public static final String INVALIDATED_METADATA_NAME = TOKEN_METADATA_NAMESPACE.concat("invalidated");
+
+		/**
+		 * The name of the metadata used for the claims of the token.
+		 */
+		public static final String CLAIMS_METADATA_NAME = TOKEN_METADATA_NAMESPACE.concat("claims");
+
+		private final T token;
+
+		private final Map<String, Object> metadata;
+
+		protected Token(T token) {
+			this(token, defaultMetadata());
+		}
+
+		protected Token(T token, Map<String, Object> metadata) {
+			this.token = token;
+			this.metadata = Collections.unmodifiableMap(metadata);
+		}
+
+		/**
+		 * Returns the token of type {@link OAuth2Token}.
+		 * @return the token of type {@link OAuth2Token}
+		 */
+		public T getToken() {
+			return this.token;
+		}
+
+		/**
+		 * Returns {@code true} if the token has been invalidated (e.g. revoked). The
+		 * default is {@code false}.
+		 * @return {@code true} if the token has been invalidated, {@code false} otherwise
+		 */
+		public boolean isInvalidated() {
+			return Boolean.TRUE.equals(getMetadata(INVALIDATED_METADATA_NAME));
+		}
+
+		/**
+		 * Returns {@code true} if the token has expired.
+		 * @return {@code true} if the token has expired, {@code false} otherwise
+		 */
+		public boolean isExpired() {
+			return getToken().getExpiresAt() != null && Instant.now().isAfter(getToken().getExpiresAt());
+		}
+
+		/**
+		 * Returns {@code true} if the token is before the time it can be used.
+		 * @return {@code true} if the token is before the time it can be used,
+		 * {@code false} otherwise
+		 */
+		public boolean isBeforeUse() {
+			Instant notBefore = null;
+			if (!CollectionUtils.isEmpty(getClaims())) {
+				notBefore = (Instant) getClaims().get("nbf");
+			}
+			return notBefore != null && Instant.now().isBefore(notBefore);
+		}
+
+		/**
+		 * Returns {@code true} if the token is currently active.
+		 * @return {@code true} if the token is currently active, {@code false} otherwise
+		 */
+		public boolean isActive() {
+			return !isInvalidated() && !isExpired() && !isBeforeUse();
+		}
+
+		/**
+		 * Returns the claims associated to the token.
+		 * @return a {@code Map} of the claims, or {@code null} if not available
+		 */
+		@Nullable
+		public Map<String, Object> getClaims() {
+			return getMetadata(CLAIMS_METADATA_NAME);
+		}
+
+		/**
+		 * Returns the value of the metadata associated to the token.
+		 * @param name the name of the metadata
+		 * @param <V> the value type of the metadata
+		 * @return the value of the metadata, or {@code null} if not available
+		 */
+		@Nullable
+		@SuppressWarnings("unchecked")
+		public <V> V getMetadata(String name) {
+			Assert.hasText(name, "name cannot be empty");
+			return (V) this.metadata.get(name);
+		}
+
+		/**
+		 * Returns the metadata associated to the token.
+		 * @return a {@code Map} of the metadata
+		 */
+		public Map<String, Object> getMetadata() {
+			return this.metadata;
+		}
+
+		protected static Map<String, Object> defaultMetadata() {
+			Map<String, Object> metadata = new HashMap<>();
+			metadata.put(INVALIDATED_METADATA_NAME, false);
+			return metadata;
+		}
+
+		@Override
+		public boolean equals(Object obj) {
+			if (this == obj) {
+				return true;
+			}
+			if (obj == null || getClass() != obj.getClass()) {
+				return false;
+			}
+			Token<?> that = (Token<?>) obj;
+			return Objects.equals(this.token, that.token) && Objects.equals(this.metadata, that.metadata);
+		}
+
+		@Override
+		public int hashCode() {
+			return Objects.hash(this.token, this.metadata);
+		}
+
+	}
+
+	/**
+	 * A builder for {@link OAuth2Authorization}.
+	 */
+	public static class Builder {
+
+		private String id;
+
+		private final String registeredClientId;
+
+		private String principalName;
+
+		private AuthorizationGrantType authorizationGrantType;
+
+		private Set<String> authorizedScopes;
+
+		private Map<Class<? extends OAuth2Token>, Token<?>> tokens = new HashMap<>();
+
+		private final Map<String, Object> attributes = new HashMap<>();
+
+		protected Builder(String registeredClientId) {
+			this.registeredClientId = registeredClientId;
+		}
+
+		/**
+		 * Sets the identifier for the authorization.
+		 * @param id the identifier for the authorization
+		 * @return the {@link Builder}
+		 */
+		public Builder id(String id) {
+			this.id = id;
+			return this;
+		}
+
+		/**
+		 * Sets the {@code Principal} name of the resource owner (or client).
+		 * @param principalName the {@code Principal} name of the resource owner (or
+		 * client)
+		 * @return the {@link Builder}
+		 */
+		public Builder principalName(String principalName) {
+			this.principalName = principalName;
+			return this;
+		}
+
+		/**
+		 * Sets the {@link AuthorizationGrantType authorization grant type} used for the
+		 * authorization.
+		 * @param authorizationGrantType the {@link AuthorizationGrantType}
+		 * @return the {@link Builder}
+		 */
+		public Builder authorizationGrantType(AuthorizationGrantType authorizationGrantType) {
+			this.authorizationGrantType = authorizationGrantType;
+			return this;
+		}
+
+		/**
+		 * Sets the authorized scope(s).
+		 * @param authorizedScopes the {@code Set} of authorized scope(s)
+		 * @return the {@link Builder}
+		 * @since 0.4.0
+		 */
+		public Builder authorizedScopes(Set<String> authorizedScopes) {
+			this.authorizedScopes = authorizedScopes;
+			return this;
+		}
+
+		/**
+		 * Sets the {@link OAuth2AccessToken access token}.
+		 * @param accessToken the {@link OAuth2AccessToken}
+		 * @return the {@link Builder}
+		 */
+		public Builder accessToken(OAuth2AccessToken accessToken) {
+			return token(accessToken);
+		}
+
+		/**
+		 * Sets the {@link OAuth2RefreshToken refresh token}.
+		 * @param refreshToken the {@link OAuth2RefreshToken}
+		 * @return the {@link Builder}
+		 */
+		public Builder refreshToken(OAuth2RefreshToken refreshToken) {
+			return token(refreshToken);
+		}
+
+		/**
+		 * Sets the {@link OAuth2Token token}.
+		 * @param token the token
+		 * @param <T> the type of the token
+		 * @return the {@link Builder}
+		 */
+		public <T extends OAuth2Token> Builder token(T token) {
+			return token(token, (metadata) -> {
+			});
+		}
+
+		/**
+		 * Sets the {@link OAuth2Token token} and associated metadata.
+		 * @param token the token
+		 * @param metadataConsumer a {@code Consumer} of the metadata {@code Map}
+		 * @param <T> the type of the token
+		 * @return the {@link Builder}
+		 */
+		public <T extends OAuth2Token> Builder token(T token, Consumer<Map<String, Object>> metadataConsumer) {
+			Assert.notNull(token, "token cannot be null");
+			Map<String, Object> metadata = Token.defaultMetadata();
+			Token<?> existingToken = this.tokens.get(token.getClass());
+			if (existingToken != null) {
+				metadata.putAll(existingToken.getMetadata());
+			}
+			metadataConsumer.accept(metadata);
+			Class<? extends OAuth2Token> tokenClass = token.getClass();
+			this.tokens.put(tokenClass, new Token<>(token, metadata));
+			return this;
+		}
+
+		/**
+		 * Invalidates the {@link OAuth2Token token}.
+		 * @param token the token
+		 * @param <T> the type of the token
+		 * @return the {@link Builder}
+		 * @since 1.4
+		 */
+		public <T extends OAuth2Token> Builder invalidate(T token) {
+			Assert.notNull(token, "token cannot be null");
+			if (this.tokens.get(token.getClass()) == null) {
+				return this;
+			}
+			token(token, (metadata) -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true));
+			if (OAuth2RefreshToken.class.isAssignableFrom(token.getClass())) {
+				Token<?> accessToken = this.tokens.get(OAuth2AccessToken.class);
+				token(accessToken.getToken(),
+						(metadata) -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true));
+
+				Token<?> authorizationCode = this.tokens.get(OAuth2AuthorizationCode.class);
+				if (authorizationCode != null && !authorizationCode.isInvalidated()) {
+					token(authorizationCode.getToken(),
+							(metadata) -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true));
+				}
+			}
+			return this;
+		}
+
+		protected final Builder tokens(Map<Class<? extends OAuth2Token>, Token<?>> tokens) {
+			this.tokens = new HashMap<>(tokens);
+			return this;
+		}
+
+		/**
+		 * Adds an attribute associated to the authorization.
+		 * @param name the name of the attribute
+		 * @param value the value of the attribute
+		 * @return the {@link Builder}
+		 */
+		public Builder attribute(String name, Object value) {
+			Assert.hasText(name, "name cannot be empty");
+			Assert.notNull(value, "value cannot be null");
+			this.attributes.put(name, value);
+			return this;
+		}
+
+		/**
+		 * A {@code Consumer} of the attributes {@code Map} allowing the ability to add,
+		 * replace, or remove.
+		 * @param attributesConsumer a {@link Consumer} of the attributes {@code Map}
+		 * @return the {@link Builder}
+		 */
+		public Builder attributes(Consumer<Map<String, Object>> attributesConsumer) {
+			attributesConsumer.accept(this.attributes);
+			return this;
+		}
+
+		/**
+		 * Builds a new {@link OAuth2Authorization}.
+		 * @return the {@link OAuth2Authorization}
+		 */
+		public OAuth2Authorization build() {
+			Assert.hasText(this.principalName, "principalName cannot be empty");
+			Assert.notNull(this.authorizationGrantType, "authorizationGrantType cannot be null");
+
+			OAuth2Authorization authorization = new OAuth2Authorization();
+			if (!StringUtils.hasText(this.id)) {
+				this.id = UUID.randomUUID().toString();
+			}
+			authorization.id = this.id;
+			authorization.registeredClientId = this.registeredClientId;
+			authorization.principalName = this.principalName;
+			authorization.authorizationGrantType = this.authorizationGrantType;
+			authorization.authorizedScopes = Collections.unmodifiableSet(!CollectionUtils.isEmpty(this.authorizedScopes)
+					? new HashSet<>(this.authorizedScopes) : new HashSet<>());
+			authorization.tokens = Collections.unmodifiableMap(this.tokens);
+			authorization.attributes = Collections.unmodifiableMap(this.attributes);
+			return authorization;
+		}
+
+	}
+
+}

+ 44 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationCode.java

@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+import java.time.Instant;
+
+import org.springframework.security.oauth2.core.AbstractOAuth2Token;
+
+/**
+ * An implementation of an {@link AbstractOAuth2Token} representing an OAuth 2.0
+ * Authorization Code Grant.
+ *
+ * @author Joe Grandja
+ * @since 0.0.3
+ * @see AbstractOAuth2Token
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section
+ * 4.1 Authorization Code Grant</a>
+ */
+public class OAuth2AuthorizationCode extends AbstractOAuth2Token {
+
+	/**
+	 * Constructs an {@code OAuth2AuthorizationCode} using the provided parameters.
+	 * @param tokenValue the token value
+	 * @param issuedAt the time at which the token was issued
+	 * @param expiresAt the time at which the token expires
+	 */
+	public OAuth2AuthorizationCode(String tokenValue, Instant issuedAt, Instant expiresAt) {
+		super(tokenValue, issuedAt, expiresAt);
+	}
+
+}

+ 224 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationConsent.java

@@ -0,0 +1,224 @@
+/*
+ * Copyright 2020-2025 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;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.springframework.lang.NonNull;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * A representation of an OAuth 2.0 "consent" to an Authorization request, which holds
+ * state related to the set of {@link #getAuthorities() authorities} granted to a
+ * {@link #getRegisteredClientId() client} by the {@link #getPrincipalName() resource
+ * owner}.
+ * <p>
+ * When authorizing access for a given client, the resource owner may only grant a subset
+ * of the authorities the client requested. The typical use-case is the
+ * {@code authorization_code} flow, in which the client requests a set of {@code scope}s.
+ * The resource owner then selects which scopes they grant to the client.
+ *
+ * @author Daniel Garnier-Moiroux
+ * @since 0.1.2
+ */
+public final class OAuth2AuthorizationConsent implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = -1950648027021276018L;
+
+	private static final String AUTHORITIES_SCOPE_PREFIX = "SCOPE_";
+
+	private final String registeredClientId;
+
+	private final String principalName;
+
+	private final Set<GrantedAuthority> authorities;
+
+	private OAuth2AuthorizationConsent(String registeredClientId, String principalName,
+			Set<GrantedAuthority> authorities) {
+		this.registeredClientId = registeredClientId;
+		this.principalName = principalName;
+		this.authorities = Collections.unmodifiableSet(authorities);
+	}
+
+	/**
+	 * Returns the identifier for the {@link RegisteredClient#getId() registered client}.
+	 * @return the {@link RegisteredClient#getId()}
+	 */
+	public String getRegisteredClientId() {
+		return this.registeredClientId;
+	}
+
+	/**
+	 * Returns the {@code Principal} name of the resource owner (or client).
+	 * @return the {@code Principal} name of the resource owner (or client)
+	 */
+	public String getPrincipalName() {
+		return this.principalName;
+	}
+
+	/**
+	 * Returns the {@link GrantedAuthority authorities} granted to the client by the
+	 * principal.
+	 * @return the {@link GrantedAuthority authorities} granted to the client by the
+	 * principal.
+	 */
+	public Set<GrantedAuthority> getAuthorities() {
+		return this.authorities;
+	}
+
+	/**
+	 * Convenience method for obtaining the {@code scope}s granted to the client by the
+	 * principal, extracted from the {@link #getAuthorities() authorities}.
+	 * @return the {@code scope}s granted to the client by the principal.
+	 */
+	public Set<String> getScopes() {
+		Set<String> authorities = new HashSet<>();
+		for (GrantedAuthority authority : getAuthorities()) {
+			if (authority.getAuthority().startsWith(AUTHORITIES_SCOPE_PREFIX)) {
+				authorities.add(authority.getAuthority().substring(AUTHORITIES_SCOPE_PREFIX.length()));
+			}
+		}
+		return authorities;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (obj == null || getClass() != obj.getClass()) {
+			return false;
+		}
+		OAuth2AuthorizationConsent that = (OAuth2AuthorizationConsent) obj;
+		return Objects.equals(this.registeredClientId, that.registeredClientId)
+				&& Objects.equals(this.principalName, that.principalName)
+				&& Objects.equals(this.authorities, that.authorities);
+	}
+
+	@Override
+	public int hashCode() {
+		return Objects.hash(this.registeredClientId, this.principalName, this.authorities);
+	}
+
+	/**
+	 * Returns a new {@link Builder}, initialized with the values from the provided
+	 * {@code OAuth2AuthorizationConsent}.
+	 * @param authorizationConsent the {@code OAuth2AuthorizationConsent} used for
+	 * initializing the {@link Builder}
+	 * @return the {@link Builder}
+	 */
+	public static Builder from(OAuth2AuthorizationConsent authorizationConsent) {
+		Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
+		return new Builder(authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName(),
+				authorizationConsent.getAuthorities());
+	}
+
+	/**
+	 * Returns a new {@link Builder}, initialized with the given
+	 * {@link RegisteredClient#getClientId() registeredClientId} and {@code Principal}
+	 * name.
+	 * @param registeredClientId the {@link RegisteredClient#getId()}
+	 * @param principalName the {@code Principal} name
+	 * @return the {@link Builder}
+	 */
+	public static Builder withId(@NonNull String registeredClientId, @NonNull String principalName) {
+		Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
+		Assert.hasText(principalName, "principalName cannot be empty");
+		return new Builder(registeredClientId, principalName);
+	}
+
+	/**
+	 * A builder for {@link OAuth2AuthorizationConsent}.
+	 */
+	public static final class Builder {
+
+		private final String registeredClientId;
+
+		private final String principalName;
+
+		private final Set<GrantedAuthority> authorities = new HashSet<>();
+
+		private Builder(String registeredClientId, String principalName) {
+			this(registeredClientId, principalName, Collections.emptySet());
+		}
+
+		private Builder(String registeredClientId, String principalName, Set<GrantedAuthority> authorities) {
+			this.registeredClientId = registeredClientId;
+			this.principalName = principalName;
+			if (!CollectionUtils.isEmpty(authorities)) {
+				this.authorities.addAll(authorities);
+			}
+		}
+
+		/**
+		 * Adds a scope to the collection of {@code authorities} in the resulting
+		 * {@link OAuth2AuthorizationConsent}, wrapping it in a
+		 * {@link SimpleGrantedAuthority}, prefixed by {@code SCOPE_}. For example, a
+		 * {@code message.write} scope would be stored as {@code SCOPE_message.write}.
+		 * @param scope the scope
+		 * @return the {@code Builder} for further configuration
+		 */
+		public Builder scope(String scope) {
+			authority(new SimpleGrantedAuthority(AUTHORITIES_SCOPE_PREFIX + scope));
+			return this;
+		}
+
+		/**
+		 * Adds a {@link GrantedAuthority} to the collection of {@code authorities} in the
+		 * resulting {@link OAuth2AuthorizationConsent}.
+		 * @param authority the {@link GrantedAuthority}
+		 * @return the {@code Builder} for further configuration
+		 */
+		public Builder authority(GrantedAuthority authority) {
+			this.authorities.add(authority);
+			return this;
+		}
+
+		/**
+		 * A {@code Consumer} of the {@code authorities}, allowing the ability to add,
+		 * replace or remove.
+		 * @param authoritiesConsumer a {@code Consumer} of the {@code authorities}
+		 * @return the {@code Builder} for further configuration
+		 */
+		public Builder authorities(Consumer<Set<GrantedAuthority>> authoritiesConsumer) {
+			authoritiesConsumer.accept(this.authorities);
+			return this;
+		}
+
+		/**
+		 * Validate the authorities and build the {@link OAuth2AuthorizationConsent}.
+		 * There must be at least one {@link GrantedAuthority}.
+		 * @return the {@link OAuth2AuthorizationConsent}
+		 */
+		public OAuth2AuthorizationConsent build() {
+			Assert.notEmpty(this.authorities, "authorities cannot be empty");
+			return new OAuth2AuthorizationConsent(this.registeredClientId, this.principalName, this.authorities);
+		}
+
+	}
+
+}

+ 55 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationConsentService.java

@@ -0,0 +1,55 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization;
+
+import java.security.Principal;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+
+/**
+ * Implementations of this interface are responsible for the management of
+ * {@link OAuth2AuthorizationConsent OAuth 2.0 Authorization Consent(s)}.
+ *
+ * @author Daniel Garnier-Moiroux
+ * @since 0.1.2
+ * @see OAuth2AuthorizationConsent
+ */
+public interface OAuth2AuthorizationConsentService {
+
+	/**
+	 * Saves the {@link OAuth2AuthorizationConsent}.
+	 * @param authorizationConsent the {@link OAuth2AuthorizationConsent}
+	 */
+	void save(OAuth2AuthorizationConsent authorizationConsent);
+
+	/**
+	 * Removes the {@link OAuth2AuthorizationConsent}.
+	 * @param authorizationConsent the {@link OAuth2AuthorizationConsent}
+	 */
+	void remove(OAuth2AuthorizationConsent authorizationConsent);
+
+	/**
+	 * Returns the {@link OAuth2AuthorizationConsent} identified by the provided
+	 * {@code registeredClientId} and {@code principalName}, or {@code null} if not found.
+	 * @param registeredClientId the identifier for the {@link RegisteredClient}
+	 * @param principalName the name of the {@link Principal}
+	 * @return the {@link OAuth2AuthorizationConsent} if found, otherwise {@code null}
+	 */
+	@Nullable
+	OAuth2AuthorizationConsent findById(String registeredClientId, String principalName);
+
+}

+ 86 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadata.java

@@ -0,0 +1,86 @@
+/*
+ * Copyright 2020-2025 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;
+
+import java.io.Serial;
+import java.util.Map;
+
+import org.springframework.util.Assert;
+
+/**
+ * A representation of an OAuth 2.0 Authorization Server Metadata response, which is
+ * returned from an OAuth 2.0 Authorization Server's Metadata Endpoint, and contains a set
+ * of claims about the Authorization Server's configuration. The claims are defined by the
+ * OAuth 2.0 Authorization Server Metadata specification (RFC 8414).
+ *
+ * @author Daniel Garnier-Moiroux
+ * @since 0.1.1
+ * @see AbstractOAuth2AuthorizationServerMetadata
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-3.2">3.2.
+ * Authorization Server Metadata Response</a>
+ */
+public final class OAuth2AuthorizationServerMetadata extends AbstractOAuth2AuthorizationServerMetadata {
+
+	@Serial
+	private static final long serialVersionUID = 3993358339217009284L;
+
+	private OAuth2AuthorizationServerMetadata(Map<String, Object> claims) {
+		super(claims);
+	}
+
+	/**
+	 * Constructs a new {@link Builder} with empty claims.
+	 * @return the {@link Builder}
+	 */
+	public static Builder builder() {
+		return new Builder();
+	}
+
+	/**
+	 * Constructs a new {@link Builder} with the provided claims.
+	 * @param claims the claims to initialize the builder
+	 * @return the {@link Builder}
+	 */
+	public static Builder withClaims(Map<String, Object> claims) {
+		Assert.notEmpty(claims, "claims cannot be empty");
+		return new Builder().claims((c) -> c.putAll(claims));
+	}
+
+	/**
+	 * Helps configure an {@link OAuth2AuthorizationServerMetadata}.
+	 */
+	public static final class Builder extends AbstractBuilder<OAuth2AuthorizationServerMetadata, Builder> {
+
+		private Builder() {
+		}
+
+		/**
+		 * Validate the claims and build the {@link OAuth2AuthorizationServerMetadata}.
+		 * <p>
+		 * The following claims are REQUIRED: {@code issuer},
+		 * {@code authorization_endpoint}, {@code token_endpoint} and
+		 * {@code response_types_supported}.
+		 * @return the {@link OAuth2AuthorizationServerMetadata}
+		 */
+		@Override
+		public OAuth2AuthorizationServerMetadata build() {
+			validate();
+			return new OAuth2AuthorizationServerMetadata(getClaims());
+		}
+
+	}
+
+}

+ 224 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java

@@ -0,0 +1,224 @@
+/*
+ * Copyright 2020-2025 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;
+
+import java.net.URL;
+import java.util.List;
+
+import org.springframework.security.oauth2.core.ClaimAccessor;
+import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
+
+/**
+ * A {@link ClaimAccessor} for the "claims" an Authorization Server describes about its
+ * configuration, used in OAuth 2.0 Authorization Server Metadata and OpenID Connect
+ * Discovery 1.0.
+ *
+ * @author Daniel Garnier-Moiroux
+ * @author Joe Grandja
+ * @since 0.1.1
+ * @see ClaimAccessor
+ * @see OAuth2AuthorizationServerMetadataClaimNames
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-2">2.
+ * Authorization Server Metadata</a>
+ * @see <a target="_blank" href=
+ * "https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">3. OpenID
+ * Provider Metadata</a>
+ * @see <a target="_blank" href="https://www.rfc-editor.org/rfc/rfc8628.html#section-4">4.
+ * Device Authorization Grant Metadata</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc8705#section-3.3">3.3 Mutual-TLS Client
+ * Certificate-Bound Access Tokens Metadata</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc9449#section-5.1">5.1 OAuth 2.0 Demonstrating
+ * Proof of Possession (DPoP) Metadata</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc9126#name-authorization-server-metada">5.
+ * OAuth 2.0 Pushed Authorization Requests Metadata</a>
+ */
+public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAccessor {
+
+	/**
+	 * Returns the {@code URL} the Authorization Server asserts as its Issuer Identifier
+	 * {@code (issuer)}.
+	 * @return the {@code URL} the Authorization Server asserts as its Issuer Identifier
+	 */
+	default URL getIssuer() {
+		return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.ISSUER);
+	}
+
+	/**
+	 * Returns the {@code URL} of the OAuth 2.0 Authorization Endpoint
+	 * {@code (authorization_endpoint)}.
+	 * @return the {@code URL} of the OAuth 2.0 Authorization Endpoint
+	 */
+	default URL getAuthorizationEndpoint() {
+		return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT);
+	}
+
+	/**
+	 * Returns the {@code URL} of the OAuth 2.0 Pushed Authorization Request Endpoint
+	 * {@code (pushed_authorization_request_endpoint)}.
+	 * @return the {@code URL} of the OAuth 2.0 Pushed Authorization Request Endpoint
+	 * @since 1.5
+	 */
+	default URL getPushedAuthorizationRequestEndpoint() {
+		return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT);
+	}
+
+	/**
+	 * Returns the {@code URL} of the OAuth 2.0 Device Authorization Endpoint
+	 * {@code (device_authorization_endpoint)}.
+	 * @return the {@code URL} of the OAuth 2.0 Device Authorization Endpoint
+	 * @since 1.1
+	 */
+	default URL getDeviceAuthorizationEndpoint() {
+		return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT);
+	}
+
+	/**
+	 * Returns the {@code URL} of the OAuth 2.0 Token Endpoint {@code (token_endpoint)}.
+	 * @return the {@code URL} of the OAuth 2.0 Token Endpoint
+	 */
+	default URL getTokenEndpoint() {
+		return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT);
+	}
+
+	/**
+	 * Returns the client authentication methods supported by the OAuth 2.0 Token Endpoint
+	 * {@code (token_endpoint_auth_methods_supported)}.
+	 * @return the client authentication methods supported by the OAuth 2.0 Token Endpoint
+	 */
+	default List<String> getTokenEndpointAuthenticationMethods() {
+		return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED);
+	}
+
+	/**
+	 * Returns the {@code URL} of the JSON Web Key Set {@code (jwks_uri)}.
+	 * @return the {@code URL} of the JSON Web Key Set
+	 */
+	default URL getJwkSetUrl() {
+		return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI);
+	}
+
+	/**
+	 * Returns the OAuth 2.0 {@code scope} values supported {@code (scopes_supported)}.
+	 * @return the OAuth 2.0 {@code scope} values supported
+	 */
+	default List<String> getScopes() {
+		return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED);
+	}
+
+	/**
+	 * Returns the OAuth 2.0 {@code response_type} values supported
+	 * {@code (response_types_supported)}.
+	 * @return the OAuth 2.0 {@code response_type} values supported
+	 */
+	default List<String> getResponseTypes() {
+		return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED);
+	}
+
+	/**
+	 * Returns the OAuth 2.0 {@code grant_type} values supported
+	 * {@code (grant_types_supported)}.
+	 * @return the OAuth 2.0 {@code grant_type} values supported
+	 */
+	default List<String> getGrantTypes() {
+		return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED);
+	}
+
+	/**
+	 * Returns the {@code URL} of the OAuth 2.0 Token Revocation Endpoint
+	 * {@code (revocation_endpoint)}.
+	 * @return the {@code URL} of the OAuth 2.0 Token Revocation Endpoint
+	 */
+	default URL getTokenRevocationEndpoint() {
+		return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT);
+	}
+
+	/**
+	 * Returns the client authentication methods supported by the OAuth 2.0 Token
+	 * Revocation Endpoint {@code (revocation_endpoint_auth_methods_supported)}.
+	 * @return the client authentication methods supported by the OAuth 2.0 Token
+	 * Revocation Endpoint
+	 */
+	default List<String> getTokenRevocationEndpointAuthenticationMethods() {
+		return getClaimAsStringList(
+				OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED);
+	}
+
+	/**
+	 * Returns the {@code URL} of the OAuth 2.0 Token Introspection Endpoint
+	 * {@code (introspection_endpoint)}.
+	 * @return the {@code URL} of the OAuth 2.0 Token Introspection Endpoint
+	 */
+	default URL getTokenIntrospectionEndpoint() {
+		return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT);
+	}
+
+	/**
+	 * Returns the client authentication methods supported by the OAuth 2.0 Token
+	 * Introspection Endpoint {@code (introspection_endpoint_auth_methods_supported)}.
+	 * @return the client authentication methods supported by the OAuth 2.0 Token
+	 * Introspection Endpoint
+	 */
+	default List<String> getTokenIntrospectionEndpointAuthenticationMethods() {
+		return getClaimAsStringList(
+				OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED);
+	}
+
+	/**
+	 * Returns the {@code URL} of the OAuth 2.0 Dynamic Client Registration Endpoint
+	 * {@code (registration_endpoint)}.
+	 * @return the {@code URL} of the OAuth 2.0 Dynamic Client Registration Endpoint
+	 * @since 0.4.0
+	 */
+	default URL getClientRegistrationEndpoint() {
+		return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT);
+	}
+
+	/**
+	 * Returns the Proof Key for Code Exchange (PKCE) {@code code_challenge_method} values
+	 * supported {@code (code_challenge_methods_supported)}.
+	 * @return the {@code code_challenge_method} values supported
+	 */
+	default List<String> getCodeChallengeMethods() {
+		return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED);
+	}
+
+	/**
+	 * Returns {@code true} to indicate support for mutual-TLS client certificate-bound
+	 * access tokens {@code (tls_client_certificate_bound_access_tokens)}.
+	 * @return {@code true} to indicate support for mutual-TLS client certificate-bound
+	 * access tokens, {@code false} otherwise
+	 * @since 1.3
+	 */
+	default boolean isTlsClientCertificateBoundAccessTokens() {
+		return Boolean.TRUE.equals(getClaimAsBoolean(
+				OAuth2AuthorizationServerMetadataClaimNames.TLS_CLIENT_CERTIFICATE_BOUND_ACCESS_TOKENS));
+	}
+
+	/**
+	 * Returns the {@link JwsAlgorithms JSON Web Signature (JWS) algorithms} supported for
+	 * DPoP Proof JWTs {@code (dpop_signing_alg_values_supported)}.
+	 * @return the {@link JwsAlgorithms JSON Web Signature (JWS) algorithms} supported for
+	 * DPoP Proof JWTs
+	 * @since 1.5
+	 */
+	default List<String> getDPoPSigningAlgorithms() {
+		return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.DPOP_SIGNING_ALG_VALUES_SUPPORTED);
+	}
+
+}

+ 158 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java

@@ -0,0 +1,158 @@
+/*
+ * Copyright 2020-2025 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;
+
+import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
+
+/**
+ * The names of the "claims" an Authorization Server describes about its configuration,
+ * used in OAuth 2.0 Authorization Server Metadata and OpenID Connect Discovery 1.0.
+ *
+ * @author Daniel Garnier-Moiroux
+ * @author Joe Grandja
+ * @since 0.1.1
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-2">2.
+ * Authorization Server Metadata</a>
+ * @see <a target="_blank" href=
+ * "https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">3. OpenID
+ * Provider Metadata</a>
+ * @see <a target="_blank" href="https://www.rfc-editor.org/rfc/rfc8628.html#section-4">4.
+ * Device Authorization Grant Metadata</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc8705#section-3.3">3.3 Mutual-TLS Client
+ * Certificate-Bound Access Tokens Metadata</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc9449#section-5.1">5.1 OAuth 2.0 Demonstrating
+ * Proof of Possession (DPoP) Metadata</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc9126#name-authorization-server-metada">5.
+ * OAuth 2.0 Pushed Authorization Requests Metadata</a>
+ */
+public class OAuth2AuthorizationServerMetadataClaimNames {
+
+	/**
+	 * {@code issuer} - the {@code URL} the Authorization Server asserts as its Issuer
+	 * Identifier
+	 */
+	public static final String ISSUER = "issuer";
+
+	/**
+	 * {@code authorization_endpoint} - the {@code URL} of the OAuth 2.0 Authorization
+	 * Endpoint
+	 */
+	public static final String AUTHORIZATION_ENDPOINT = "authorization_endpoint";
+
+	/**
+	 * {@code pushed_authorization_request_endpoint} - the {@code URL} of the OAuth 2.0
+	 * Pushed Authorization Request Endpoint
+	 * @since 1.5
+	 */
+	public static final String PUSHED_AUTHORIZATION_REQUEST_ENDPOINT = "pushed_authorization_request_endpoint";
+
+	/**
+	 * {@code device_authorization_endpoint} - the {@code URL} of the OAuth 2.0 Device
+	 * Authorization Endpoint
+	 * @since 1.1
+	 */
+	public static final String DEVICE_AUTHORIZATION_ENDPOINT = "device_authorization_endpoint";
+
+	/**
+	 * {@code token_endpoint} - the {@code URL} of the OAuth 2.0 Token Endpoint
+	 */
+	public static final String TOKEN_ENDPOINT = "token_endpoint";
+
+	/**
+	 * {@code token_endpoint_auth_methods_supported} - the client authentication methods
+	 * supported by the OAuth 2.0 Token Endpoint
+	 */
+	public static final String TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED = "token_endpoint_auth_methods_supported";
+
+	/**
+	 * {@code jwks_uri} - the {@code URL} of the JSON Web Key Set
+	 */
+	public static final String JWKS_URI = "jwks_uri";
+
+	/**
+	 * {@code scopes_supported} - the OAuth 2.0 {@code scope} values supported
+	 */
+	public static final String SCOPES_SUPPORTED = "scopes_supported";
+
+	/**
+	 * {@code response_types_supported} - the OAuth 2.0 {@code response_type} values
+	 * supported
+	 */
+	public static final String RESPONSE_TYPES_SUPPORTED = "response_types_supported";
+
+	/**
+	 * {@code grant_types_supported} - the OAuth 2.0 {@code grant_type} values supported
+	 */
+	public static final String GRANT_TYPES_SUPPORTED = "grant_types_supported";
+
+	/**
+	 * {@code revocation_endpoint} - the {@code URL} of the OAuth 2.0 Token Revocation
+	 * Endpoint
+	 */
+	public static final String REVOCATION_ENDPOINT = "revocation_endpoint";
+
+	/**
+	 * {@code revocation_endpoint_auth_methods_supported} - the client authentication
+	 * methods supported by the OAuth 2.0 Token Revocation Endpoint
+	 */
+	public static final String REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED = "revocation_endpoint_auth_methods_supported";
+
+	/**
+	 * {@code introspection_endpoint} - the {@code URL} of the OAuth 2.0 Token
+	 * Introspection Endpoint
+	 */
+	public static final String INTROSPECTION_ENDPOINT = "introspection_endpoint";
+
+	/**
+	 * {@code introspection_endpoint_auth_methods_supported} - the client authentication
+	 * methods supported by the OAuth 2.0 Token Introspection Endpoint
+	 */
+	public static final String INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED = "introspection_endpoint_auth_methods_supported";
+
+	/**
+	 * {@code registration_endpoint} - the {@code URL} of the OAuth 2.0 Dynamic Client
+	 * Registration Endpoint
+	 * @since 0.4.0
+	 */
+	public static final String REGISTRATION_ENDPOINT = "registration_endpoint";
+
+	/**
+	 * {@code code_challenge_methods_supported} - the Proof Key for Code Exchange (PKCE)
+	 * {@code code_challenge_method} values supported
+	 */
+	public static final String CODE_CHALLENGE_METHODS_SUPPORTED = "code_challenge_methods_supported";
+
+	/**
+	 * {@code tls_client_certificate_bound_access_tokens} - {@code true} to indicate
+	 * support for mutual-TLS client certificate-bound access tokens
+	 * @since 1.3
+	 */
+	public static final String TLS_CLIENT_CERTIFICATE_BOUND_ACCESS_TOKENS = "tls_client_certificate_bound_access_tokens";
+
+	/**
+	 * {@code dpop_signing_alg_values_supported} - the {@link JwsAlgorithms JSON Web
+	 * Signature (JWS) algorithms} supported for DPoP Proof JWTs
+	 * @since 1.5
+	 */
+	public static final String DPOP_SIGNING_ALG_VALUES_SUPPORTED = "dpop_signing_alg_values_supported";
+
+	protected OAuth2AuthorizationServerMetadataClaimNames() {
+	}
+
+}

+ 62 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationService.java

@@ -0,0 +1,62 @@
+/*
+ * 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;
+
+import org.springframework.lang.Nullable;
+
+/**
+ * Implementations of this interface are responsible for the management of
+ * {@link OAuth2Authorization OAuth 2.0 Authorization(s)}.
+ *
+ * @author Joe Grandja
+ * @since 0.0.1
+ * @see OAuth2Authorization
+ * @see OAuth2TokenType
+ */
+public interface OAuth2AuthorizationService {
+
+	/**
+	 * Saves the {@link OAuth2Authorization}.
+	 * @param authorization the {@link OAuth2Authorization}
+	 */
+	void save(OAuth2Authorization authorization);
+
+	/**
+	 * Removes the {@link OAuth2Authorization}.
+	 * @param authorization the {@link OAuth2Authorization}
+	 */
+	void remove(OAuth2Authorization authorization);
+
+	/**
+	 * Returns the {@link OAuth2Authorization} identified by the provided {@code id}, or
+	 * {@code null} if not found.
+	 * @param id the authorization identifier
+	 * @return the {@link OAuth2Authorization} if found, otherwise {@code null}
+	 */
+	@Nullable
+	OAuth2Authorization findById(String id);
+
+	/**
+	 * Returns the {@link OAuth2Authorization} containing the provided {@code token}, or
+	 * {@code null} if not found.
+	 * @param token the token credential
+	 * @param tokenType the {@link OAuth2TokenType token type}
+	 * @return the {@link OAuth2Authorization} if found, otherwise {@code null}
+	 */
+	@Nullable
+	OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType);
+
+}

+ 344 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenIntrospection.java

@@ -0,0 +1,344 @@
+/*
+ * Copyright 2020-2025 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;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.net.URI;
+import java.net.URL;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor;
+import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
+import org.springframework.util.Assert;
+
+/**
+ * A representation of the claims returned in an OAuth 2.0 Token Introspection Response.
+ *
+ * @author Gerardo Roza
+ * @author Joe Grandja
+ * @since 0.1.1
+ * @see OAuth2TokenIntrospectionClaimAccessor
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7662#section-2.2">Section
+ * 2.2 Introspection Response</a>
+ */
+public final class OAuth2TokenIntrospection implements OAuth2TokenIntrospectionClaimAccessor, Serializable {
+
+	@Serial
+	private static final long serialVersionUID = -8846164058150912395L;
+
+	private final Map<String, Object> claims;
+
+	private OAuth2TokenIntrospection(Map<String, Object> claims) {
+		this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
+	}
+
+	/**
+	 * Returns the claims in the Token Introspection Response.
+	 * @return a {@code Map} of the claims
+	 */
+	@Override
+	public Map<String, Object> getClaims() {
+		return this.claims;
+	}
+
+	/**
+	 * Constructs a new {@link Builder} initialized with the {@link #isActive() active}
+	 * claim to {@code false}.
+	 * @return the {@link Builder}
+	 */
+	public static Builder builder() {
+		return builder(false);
+	}
+
+	/**
+	 * Constructs a new {@link Builder} initialized with the provided {@link #isActive()
+	 * active} claim.
+	 * @param active {@code true} if the token is currently active, {@code false}
+	 * otherwise
+	 * @return the {@link Builder}
+	 */
+	public static Builder builder(boolean active) {
+		return new Builder(active);
+	}
+
+	/**
+	 * Constructs a new {@link Builder} initialized with the provided claims.
+	 * @param claims the claims to initialize the builder
+	 * @return the {@link Builder}
+	 */
+	public static Builder withClaims(Map<String, Object> claims) {
+		Assert.notEmpty(claims, "claims cannot be empty");
+		return builder().claims((c) -> c.putAll(claims));
+	}
+
+	/**
+	 * A builder for {@link OAuth2TokenIntrospection}.
+	 */
+	public static final class Builder {
+
+		private final Map<String, Object> claims = new LinkedHashMap<>();
+
+		private Builder(boolean active) {
+			active(active);
+		}
+
+		/**
+		 * Sets the indicator of whether or not the presented token is currently active,
+		 * REQUIRED.
+		 * @param active {@code true} if the token is currently active, {@code false}
+		 * otherwise
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder active(boolean active) {
+			return claim(OAuth2TokenIntrospectionClaimNames.ACTIVE, active);
+		}
+
+		/**
+		 * Add the scope associated with this token, OPTIONAL.
+		 * @param scope the scope associated with this token
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder scope(String scope) {
+			addClaimToClaimList(OAuth2TokenIntrospectionClaimNames.SCOPE, scope);
+			return this;
+		}
+
+		/**
+		 * A {@code Consumer} of the scope(s) associated with this token, allowing the
+		 * ability to add, replace, or remove, OPTIONAL.
+		 * @param scopesConsumer a {@code Consumer} of the scope(s) associated with this
+		 * token
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder scopes(Consumer<List<String>> scopesConsumer) {
+			acceptClaimValues(OAuth2TokenIntrospectionClaimNames.SCOPE, scopesConsumer);
+			return this;
+		}
+
+		/**
+		 * Sets the client identifier for the OAuth 2.0 client that requested this token,
+		 * OPTIONAL.
+		 * @param clientId the client identifier for the OAuth 2.0 client that requested
+		 * this token
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder clientId(String clientId) {
+			return claim(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, clientId);
+		}
+
+		/**
+		 * Sets the human-readable identifier for the resource owner who authorized this
+		 * token, OPTIONAL.
+		 * @param username the human-readable identifier for the resource owner who
+		 * authorized this token
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder username(String username) {
+			return claim(OAuth2TokenIntrospectionClaimNames.USERNAME, username);
+		}
+
+		/**
+		 * Sets the token type (e.g. bearer), OPTIONAL.
+		 * @param tokenType the token type
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder tokenType(String tokenType) {
+			return claim(OAuth2TokenIntrospectionClaimNames.TOKEN_TYPE, tokenType);
+		}
+
+		/**
+		 * Sets the time indicating when this token will expire, OPTIONAL.
+		 * @param expiresAt the time indicating when this token will expire
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder expiresAt(Instant expiresAt) {
+			return claim(OAuth2TokenIntrospectionClaimNames.EXP, expiresAt);
+		}
+
+		/**
+		 * Sets the time indicating when this token was originally issued, OPTIONAL.
+		 * @param issuedAt the time indicating when this token was originally issued
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder issuedAt(Instant issuedAt) {
+			return claim(OAuth2TokenIntrospectionClaimNames.IAT, issuedAt);
+		}
+
+		/**
+		 * Sets the time indicating when this token is not to be used before, OPTIONAL.
+		 * @param notBefore the time indicating when this token is not to be used before
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder notBefore(Instant notBefore) {
+			return claim(OAuth2TokenIntrospectionClaimNames.NBF, notBefore);
+		}
+
+		/**
+		 * Sets the subject of the token, usually a machine-readable identifier of the
+		 * resource owner who authorized this token, OPTIONAL.
+		 * @param subject the subject of the token
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder subject(String subject) {
+			return claim(OAuth2TokenIntrospectionClaimNames.SUB, subject);
+		}
+
+		/**
+		 * Add the identifier representing the intended audience for this token, OPTIONAL.
+		 * @param audience the identifier representing the intended audience for this
+		 * token
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder audience(String audience) {
+			addClaimToClaimList(OAuth2TokenIntrospectionClaimNames.AUD, audience);
+			return this;
+		}
+
+		/**
+		 * A {@code Consumer} of the intended audience(s) for this token, allowing the
+		 * ability to add, replace, or remove, OPTIONAL.
+		 * @param audiencesConsumer a {@code Consumer} of the intended audience(s) for
+		 * this token
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder audiences(Consumer<List<String>> audiencesConsumer) {
+			acceptClaimValues(OAuth2TokenIntrospectionClaimNames.AUD, audiencesConsumer);
+			return this;
+		}
+
+		/**
+		 * Sets the issuer of this token, OPTIONAL.
+		 * @param issuer the issuer of this token
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder issuer(String issuer) {
+			return claim(OAuth2TokenIntrospectionClaimNames.ISS, issuer);
+		}
+
+		/**
+		 * Sets the identifier for the token, OPTIONAL.
+		 * @param jti the identifier for the token
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder id(String jti) {
+			return claim(OAuth2TokenIntrospectionClaimNames.JTI, jti);
+		}
+
+		/**
+		 * Sets the claim.
+		 * @param name the claim name
+		 * @param value the claim value
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder claim(String name, Object value) {
+			Assert.hasText(name, "name cannot be empty");
+			Assert.notNull(value, "value cannot be null");
+			this.claims.put(name, value);
+			return this;
+		}
+
+		/**
+		 * Provides access to every {@link #claim(String, Object)} declared so far with
+		 * the possibility to add, replace, or remove.
+		 * @param claimsConsumer a {@code Consumer} of the claims
+		 * @return the {@link Builder} for further configurations
+		 */
+		public Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
+			claimsConsumer.accept(this.claims);
+			return this;
+		}
+
+		/**
+		 * Validate the claims and build the {@link OAuth2TokenIntrospection}.
+		 * <p>
+		 * The following claims are REQUIRED: {@code active}
+		 * @return the {@link OAuth2TokenIntrospection}
+		 */
+		public OAuth2TokenIntrospection build() {
+			validate();
+			return new OAuth2TokenIntrospection(this.claims);
+		}
+
+		private void validate() {
+			Assert.notNull(this.claims.get(OAuth2TokenIntrospectionClaimNames.ACTIVE), "active cannot be null");
+			Assert.isInstanceOf(Boolean.class, this.claims.get(OAuth2TokenIntrospectionClaimNames.ACTIVE),
+					"active must be of type boolean");
+			if (this.claims.containsKey(OAuth2TokenIntrospectionClaimNames.SCOPE)) {
+				Assert.isInstanceOf(List.class, this.claims.get(OAuth2TokenIntrospectionClaimNames.SCOPE),
+						"scope must be of type List");
+			}
+			if (this.claims.containsKey(OAuth2TokenIntrospectionClaimNames.EXP)) {
+				Assert.isInstanceOf(Instant.class, this.claims.get(OAuth2TokenIntrospectionClaimNames.EXP),
+						"exp must be of type Instant");
+			}
+			if (this.claims.containsKey(OAuth2TokenIntrospectionClaimNames.IAT)) {
+				Assert.isInstanceOf(Instant.class, this.claims.get(OAuth2TokenIntrospectionClaimNames.IAT),
+						"iat must be of type Instant");
+			}
+			if (this.claims.containsKey(OAuth2TokenIntrospectionClaimNames.NBF)) {
+				Assert.isInstanceOf(Instant.class, this.claims.get(OAuth2TokenIntrospectionClaimNames.NBF),
+						"nbf must be of type Instant");
+			}
+			if (this.claims.containsKey(OAuth2TokenIntrospectionClaimNames.AUD)) {
+				Assert.isInstanceOf(List.class, this.claims.get(OAuth2TokenIntrospectionClaimNames.AUD),
+						"aud must be of type List");
+			}
+			if (this.claims.containsKey(OAuth2TokenIntrospectionClaimNames.ISS)) {
+				validateURL(this.claims.get(OAuth2TokenIntrospectionClaimNames.ISS), "iss must be a valid URL");
+			}
+		}
+
+		@SuppressWarnings("unchecked")
+		private void addClaimToClaimList(String name, String value) {
+			Assert.hasText(name, "name cannot be empty");
+			Assert.notNull(value, "value cannot be null");
+			this.claims.computeIfAbsent(name, (k) -> new LinkedList<String>());
+			((List<String>) this.claims.get(name)).add(value);
+		}
+
+		@SuppressWarnings("unchecked")
+		private void acceptClaimValues(String name, Consumer<List<String>> valuesConsumer) {
+			Assert.hasText(name, "name cannot be empty");
+			Assert.notNull(valuesConsumer, "valuesConsumer cannot be null");
+			this.claims.computeIfAbsent(name, (k) -> new LinkedList<String>());
+			List<String> values = (List<String>) this.claims.get(name);
+			valuesConsumer.accept(values);
+		}
+
+		private static void validateURL(Object url, String errorMessage) {
+			if (URL.class.isAssignableFrom(url.getClass())) {
+				return;
+			}
+
+			try {
+				new URI(url.toString()).toURL();
+			}
+			catch (Exception ex) {
+				throw new IllegalArgumentException(errorMessage, ex);
+			}
+		}
+
+	}
+
+}

+ 82 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenType.java

@@ -0,0 +1,82 @@
+/*
+ * Copyright 2020-2025 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;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+import org.springframework.util.Assert;
+
+/**
+ * Standard token types defined in the OAuth Token Type Hints Registry.
+ *
+ * @author Joe Grandja
+ * @since 0.0.1
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7009#section-4.1.2">4.1.2
+ * OAuth Token Type Hints Registry</a>
+ */
+public final class OAuth2TokenType implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = -9015673781220922768L;
+
+	/**
+	 * {@code access_token} token type.
+	 */
+	public static final OAuth2TokenType ACCESS_TOKEN = new OAuth2TokenType("access_token");
+
+	/**
+	 * {@code refresh_token} token type.
+	 */
+	public static final OAuth2TokenType REFRESH_TOKEN = new OAuth2TokenType("refresh_token");
+
+	private final String value;
+
+	/**
+	 * Constructs an {@code OAuth2TokenType} using the provided value.
+	 * @param value the value of the token type
+	 */
+	public OAuth2TokenType(String value) {
+		Assert.hasText(value, "value cannot be empty");
+		this.value = value;
+	}
+
+	/**
+	 * Returns the value of the token type.
+	 * @return the value of the token type
+	 */
+	public String getValue() {
+		return this.value;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (obj == null || this.getClass() != obj.getClass()) {
+			return false;
+		}
+		OAuth2TokenType that = (OAuth2TokenType) obj;
+		return getValue().equals(that.getValue());
+	}
+
+	@Override
+	public int hashCode() {
+		return getValue().hashCode();
+	}
+
+}

+ 213 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/aot/hint/OAuth2AuthorizationServerBeanRegistrationAotProcessor.java

@@ -0,0 +1,213 @@
+/*
+ * Copyright 2020-2024 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.aot.hint;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+
+import org.springframework.aot.generate.GenerationContext;
+import org.springframework.aot.hint.BindingReflectionHintsRegistrar;
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
+import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
+import org.springframework.beans.factory.aot.BeanRegistrationCode;
+import org.springframework.beans.factory.support.RegisteredBean;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.jackson2.CoreJackson2Module;
+import org.springframework.security.oauth2.core.AbstractOAuth2Token;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
+import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
+import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
+import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
+import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
+import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
+import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
+import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
+import org.springframework.security.web.authentication.WebAuthenticationDetails;
+import org.springframework.security.web.jackson2.WebServletJackson2Module;
+import org.springframework.security.web.savedrequest.DefaultSavedRequest;
+import org.springframework.util.ClassUtils;
+
+/**
+ * {@link BeanRegistrationAotProcessor} that detects specific registered beans and
+ * contributes the required {@link RuntimeHints}. Statically registered via
+ * META-INF/spring/aot.factories.
+ *
+ * @author Joe Grandja
+ * @author Josh Long
+ * @author William Koch
+ * @since 1.2
+ */
+class OAuth2AuthorizationServerBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor {
+
+	private boolean jackson2Contributed;
+
+	@Override
+	public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) {
+		boolean isJdbcBasedOAuth2AuthorizationService = JdbcOAuth2AuthorizationService.class
+			.isAssignableFrom(registeredBean.getBeanClass());
+
+		boolean isJdbcBasedRegisteredClientRepository = JdbcRegisteredClientRepository.class
+			.isAssignableFrom(registeredBean.getBeanClass());
+
+		// @formatter:off
+		if ((isJdbcBasedOAuth2AuthorizationService || isJdbcBasedRegisteredClientRepository)
+				&& !this.jackson2Contributed) {
+			Jackson2ConfigurationBeanRegistrationAotContribution jackson2Contribution =
+					new Jackson2ConfigurationBeanRegistrationAotContribution();
+			this.jackson2Contributed = true;
+			return jackson2Contribution;
+		}
+		// @formatter:on
+		return null;
+	}
+
+	private static class Jackson2ConfigurationBeanRegistrationAotContribution
+			implements BeanRegistrationAotContribution {
+
+		private final BindingReflectionHintsRegistrar reflectionHintsRegistrar = new BindingReflectionHintsRegistrar();
+
+		@Override
+		public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) {
+			registerHints(generationContext.getRuntimeHints());
+		}
+
+		private void registerHints(RuntimeHints hints) {
+			// Collections -> UnmodifiableSet, UnmodifiableList, UnmodifiableMap,
+			// UnmodifiableRandomAccessList, etc.
+			hints.reflection().registerType(Collections.class, MemberCategory.DECLARED_CLASSES);
+
+			// HashSet
+			hints.reflection()
+				.registerType(HashSet.class, MemberCategory.DECLARED_FIELDS,
+						MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS);
+
+			// Spring Security and Spring Authorization Server
+			hints.reflection()
+				.registerTypes(Arrays.asList(TypeReference.of(AbstractAuthenticationToken.class),
+						TypeReference.of(DefaultSavedRequest.Builder.class),
+						TypeReference.of(WebAuthenticationDetails.class),
+						TypeReference.of(UsernamePasswordAuthenticationToken.class), TypeReference.of(User.class),
+						TypeReference.of(DefaultOidcUser.class), TypeReference.of(DefaultOAuth2User.class),
+						TypeReference.of(OidcUserAuthority.class), TypeReference.of(OAuth2UserAuthority.class),
+						TypeReference.of(SimpleGrantedAuthority.class), TypeReference.of(OidcIdToken.class),
+						TypeReference.of(AbstractOAuth2Token.class), TypeReference.of(OidcUserInfo.class),
+						TypeReference.of(OAuth2TokenExchangeActor.class),
+						TypeReference.of(OAuth2AuthorizationRequest.class),
+						TypeReference.of(OAuth2TokenExchangeCompositeAuthenticationToken.class),
+						TypeReference.of(AuthorizationGrantType.class),
+						TypeReference.of(OAuth2AuthorizationResponseType.class),
+						TypeReference.of(OAuth2TokenFormat.class)),
+						(builder) -> builder.withMembers(MemberCategory.DECLARED_FIELDS,
+								MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS));
+
+			// Jackson Modules - Spring Security and Spring Authorization Server
+			hints.reflection()
+				.registerTypes(
+						Arrays.asList(TypeReference.of(CoreJackson2Module.class),
+								TypeReference.of(WebServletJackson2Module.class),
+								TypeReference.of(OAuth2AuthorizationServerJackson2Module.class)),
+						(builder) -> builder.withMembers(MemberCategory.DECLARED_FIELDS,
+								MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS));
+
+			// Jackson Mixins - Spring Security and Spring Authorization Server
+			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
+					loadClass("org.springframework.security.jackson2.UnmodifiableSetMixin"));
+			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
+					loadClass("org.springframework.security.jackson2.UnmodifiableListMixin"));
+			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
+					loadClass("org.springframework.security.jackson2.UnmodifiableMapMixin"));
+			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(), loadClass(
+					"org.springframework.security.oauth2.server.authorization.jackson2.UnmodifiableMapMixin"));
+			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
+					loadClass("org.springframework.security.oauth2.server.authorization.jackson2.HashSetMixin"));
+			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
+					loadClass("org.springframework.security.web.jackson2.DefaultSavedRequestMixin"));
+			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
+					loadClass("org.springframework.security.web.jackson2.WebAuthenticationDetailsMixin"));
+			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
+					loadClass("org.springframework.security.jackson2.UsernamePasswordAuthenticationTokenMixin"));
+			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
+					loadClass("org.springframework.security.jackson2.UserMixin"));
+			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
+					loadClass("org.springframework.security.jackson2.SimpleGrantedAuthorityMixin"));
+			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(), loadClass(
+					"org.springframework.security.oauth2.server.authorization.jackson2.OAuth2TokenExchangeActorMixin"));
+			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(), loadClass(
+					"org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationRequestMixin"));
+			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(), loadClass(
+					"org.springframework.security.oauth2.server.authorization.jackson2.OAuth2TokenExchangeCompositeAuthenticationTokenMixin"));
+			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(), loadClass(
+					"org.springframework.security.oauth2.server.authorization.jackson2.OAuth2TokenFormatMixin"));
+
+			// Check if Spring Security OAuth2 Client is on classpath
+			if (ClassUtils.isPresent("org.springframework.security.oauth2.client.registration.ClientRegistration",
+					ClassUtils.getDefaultClassLoader())) {
+
+				// Jackson Module (and required types) - Spring Security OAuth2 Client
+				hints.reflection()
+					.registerTypes(Arrays.asList(
+							TypeReference
+								.of("org.springframework.security.oauth2.client.jackson2.OAuth2ClientJackson2Module"),
+							TypeReference
+								.of("org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken")),
+							(builder) -> builder.withMembers(MemberCategory.DECLARED_FIELDS,
+									MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
+									MemberCategory.INVOKE_DECLARED_METHODS));
+
+				// Jackson Mixins - Spring Security OAuth2 Client
+				this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(), loadClass(
+						"org.springframework.security.oauth2.client.jackson2.OAuth2AuthenticationTokenMixin"));
+				this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
+						loadClass("org.springframework.security.oauth2.client.jackson2.DefaultOidcUserMixin"));
+				this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
+						loadClass("org.springframework.security.oauth2.client.jackson2.DefaultOAuth2UserMixin"));
+				this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
+						loadClass("org.springframework.security.oauth2.client.jackson2.OidcUserAuthorityMixin"));
+				this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
+						loadClass("org.springframework.security.oauth2.client.jackson2.OAuth2UserAuthorityMixin"));
+				this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
+						loadClass("org.springframework.security.oauth2.client.jackson2.OidcIdTokenMixin"));
+				this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
+						loadClass("org.springframework.security.oauth2.client.jackson2.OidcUserInfoMixin"));
+			}
+		}
+
+		private static Class<?> loadClass(String className) {
+			try {
+				return Class.forName(className);
+			}
+			catch (ClassNotFoundException ex) {
+				throw new RuntimeException(ex);
+			}
+		}
+
+	}
+
+}

+ 136 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/AbstractOAuth2AuthorizationCodeRequestAuthenticationToken.java

@@ -0,0 +1,136 @@
+/*
+ * Copyright 2020-2025 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.io.Serial;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} base implementation for the OAuth 2.0 Authorization Request
+ * used in the Authorization Code Grant.
+ *
+ * @author Joe Grandja
+ * @since 1.5
+ * @see OAuth2AuthorizationCodeRequestAuthenticationToken
+ * @see OAuth2PushedAuthorizationRequestAuthenticationToken
+ */
+abstract class AbstractOAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractAuthenticationToken {
+
+	@Serial
+	private static final long serialVersionUID = -5813797478091794517L;
+
+	private final String authorizationUri;
+
+	private final String clientId;
+
+	private final Authentication principal;
+
+	private final String redirectUri;
+
+	private final String state;
+
+	private final Set<String> scopes;
+
+	private final Map<String, Object> additionalParameters;
+
+	protected AbstractOAuth2AuthorizationCodeRequestAuthenticationToken(String authorizationUri, String clientId,
+			Authentication principal, @Nullable String redirectUri, @Nullable String state,
+			@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
+		super(Collections.emptyList());
+		Assert.hasText(authorizationUri, "authorizationUri cannot be empty");
+		Assert.hasText(clientId, "clientId cannot be empty");
+		Assert.notNull(principal, "principal cannot be null");
+		this.authorizationUri = authorizationUri;
+		this.clientId = clientId;
+		this.principal = principal;
+		this.redirectUri = redirectUri;
+		this.state = state;
+		this.scopes = Collections.unmodifiableSet((scopes != null) ? new HashSet<>(scopes) : Collections.emptySet());
+		this.additionalParameters = Collections.unmodifiableMap(
+				(additionalParameters != null) ? new HashMap<>(additionalParameters) : Collections.emptyMap());
+	}
+
+	@Override
+	public Object getPrincipal() {
+		return this.principal;
+	}
+
+	@Override
+	public Object getCredentials() {
+		return "";
+	}
+
+	/**
+	 * Returns the authorization URI.
+	 * @return the authorization URI
+	 */
+	public String getAuthorizationUri() {
+		return this.authorizationUri;
+	}
+
+	/**
+	 * Returns the client identifier.
+	 * @return the client identifier
+	 */
+	public String getClientId() {
+		return this.clientId;
+	}
+
+	/**
+	 * Returns the redirect uri.
+	 * @return the redirect uri
+	 */
+	@Nullable
+	public String getRedirectUri() {
+		return this.redirectUri;
+	}
+
+	/**
+	 * Returns the state.
+	 * @return the state
+	 */
+	@Nullable
+	public String getState() {
+		return this.state;
+	}
+
+	/**
+	 * Returns the requested (or authorized) scope(s).
+	 * @return the requested (or authorized) scope(s), or an empty {@code Set} if not
+	 * available
+	 */
+	public Set<String> getScopes() {
+		return this.scopes;
+	}
+
+	/**
+	 * Returns the additional parameters.
+	 * @return the additional parameters, or an empty {@code Map} if not available
+	 */
+	public Map<String, Object> getAdditionalParameters() {
+		return this.additionalParameters;
+	}
+
+}

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

@@ -0,0 +1,172 @@
+/*
+ * Copyright 2020-2023 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.time.Instant;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.log.LogMessage;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.crypto.factory.PasswordEncoderFactories;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+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.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link AuthenticationProvider} implementation used for OAuth 2.0 Client
+ * Authentication, which authenticates the {@link OAuth2ParameterNames#CLIENT_SECRET
+ * client_secret} parameter.
+ *
+ * @author Patryk Kostrzewa
+ * @author Joe Grandja
+ * @since 0.2.3
+ * @see AuthenticationProvider
+ * @see OAuth2ClientAuthenticationToken
+ * @see RegisteredClientRepository
+ * @see OAuth2AuthorizationService
+ * @see PasswordEncoder
+ */
+public final class ClientSecretAuthenticationProvider implements AuthenticationProvider {
+
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final RegisteredClientRepository registeredClientRepository;
+
+	private final CodeVerifierAuthenticator codeVerifierAuthenticator;
+
+	private PasswordEncoder passwordEncoder;
+
+	/**
+	 * Constructs a {@code ClientSecretAuthenticationProvider} using the provided
+	 * parameters.
+	 * @param registeredClientRepository the repository of registered clients
+	 * @param authorizationService the authorization service
+	 */
+	public ClientSecretAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
+			OAuth2AuthorizationService authorizationService) {
+		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		this.registeredClientRepository = registeredClientRepository;
+		this.codeVerifierAuthenticator = new CodeVerifierAuthenticator(authorizationService);
+		this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
+	}
+
+	/**
+	 * Sets the {@link PasswordEncoder} used to validate the
+	 * {@link RegisteredClient#getClientSecret() client secret}. If not set, the client
+	 * secret will be compared using
+	 * {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}.
+	 * @param passwordEncoder the {@link PasswordEncoder} used to validate the client
+	 * secret
+	 */
+	public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
+		Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
+		this.passwordEncoder = passwordEncoder;
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) authentication;
+
+		// @formatter:off
+		if (!ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientAuthentication.getClientAuthenticationMethod()) &&
+				!ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientAuthentication.getClientAuthenticationMethod())) {
+			return null;
+		}
+		// @formatter:on
+
+		String clientId = clientAuthentication.getPrincipal().toString();
+		RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
+		if (registeredClient == null) {
+			throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved registered client");
+		}
+
+		if (!registeredClient.getClientAuthenticationMethods()
+			.contains(clientAuthentication.getClientAuthenticationMethod())) {
+			throwInvalidClient("authentication_method");
+		}
+
+		if (clientAuthentication.getCredentials() == null) {
+			throwInvalidClient("credentials");
+		}
+
+		String clientSecret = clientAuthentication.getCredentials().toString();
+		if (!this.passwordEncoder.matches(clientSecret, registeredClient.getClientSecret())) {
+			if (this.logger.isDebugEnabled()) {
+				this.logger.debug(LogMessage.format(
+						"Invalid request: client_secret does not match" + " for registered client '%s'",
+						registeredClient.getId()));
+			}
+			throwInvalidClient(OAuth2ParameterNames.CLIENT_SECRET);
+		}
+
+		if (registeredClient.getClientSecretExpiresAt() != null
+				&& Instant.now().isAfter(registeredClient.getClientSecretExpiresAt())) {
+			throwInvalidClient("client_secret_expires_at");
+		}
+
+		if (this.passwordEncoder.upgradeEncoding(registeredClient.getClientSecret())) {
+			registeredClient = RegisteredClient.from(registeredClient)
+				.clientSecret(this.passwordEncoder.encode(clientSecret))
+				.build();
+			this.registeredClientRepository.save(registeredClient);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Validated client authentication parameters");
+		}
+
+		// Validate the "code_verifier" parameter for the confidential client, if
+		// available
+		this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Authenticated client secret");
+		}
+
+		return new OAuth2ClientAuthenticationToken(registeredClient,
+				clientAuthentication.getClientAuthenticationMethod(), clientAuthentication.getCredentials());
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	private static void throwInvalidClient(String parameterName) {
+		OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
+				"Client authentication failed: " + parameterName, ERROR_URI);
+		throw new OAuth2AuthenticationException(error);
+	}
+
+}

+ 177 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/CodeVerifierAuthenticator.java

@@ -0,0 +1,177 @@
+/*
+ * Copyright 2020-2024 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.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.log.LogMessage;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+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.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * An authenticator used for OAuth 2.0 Client Authentication, which authenticates the
+ * {@link PkceParameterNames#CODE_VERIFIER code_verifier} parameter.
+ *
+ * @author Daniel Garnier-Moiroux
+ * @author Joe Grandja
+ * @since 0.2.3
+ * @see OAuth2ClientAuthenticationToken
+ * @see OAuth2AuthorizationService
+ */
+final class CodeVerifierAuthenticator {
+
+	private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final OAuth2AuthorizationService authorizationService;
+
+	CodeVerifierAuthenticator(OAuth2AuthorizationService authorizationService) {
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		this.authorizationService = authorizationService;
+	}
+
+	void authenticateRequired(OAuth2ClientAuthenticationToken clientAuthentication, RegisteredClient registeredClient) {
+		if (!authenticate(clientAuthentication, registeredClient)) {
+			throwInvalidGrant(PkceParameterNames.CODE_VERIFIER);
+		}
+	}
+
+	void authenticateIfAvailable(OAuth2ClientAuthenticationToken clientAuthentication,
+			RegisteredClient registeredClient) {
+		authenticate(clientAuthentication, registeredClient);
+	}
+
+	private boolean authenticate(OAuth2ClientAuthenticationToken clientAuthentication,
+			RegisteredClient registeredClient) {
+
+		Map<String, Object> parameters = clientAuthentication.getAdditionalParameters();
+		if (!authorizationCodeGrant(parameters)) {
+			return false;
+		}
+
+		OAuth2Authorization authorization = this.authorizationService
+			.findByToken((String) parameters.get(OAuth2ParameterNames.CODE), AUTHORIZATION_CODE_TOKEN_TYPE);
+		if (authorization == null) {
+			throwInvalidGrant(OAuth2ParameterNames.CODE);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved authorization with authorization code");
+		}
+
+		OAuth2AuthorizationRequest authorizationRequest = authorization
+			.getAttribute(OAuth2AuthorizationRequest.class.getName());
+
+		String codeChallenge = (String) authorizationRequest.getAdditionalParameters()
+			.get(PkceParameterNames.CODE_CHALLENGE);
+		String codeVerifier = (String) parameters.get(PkceParameterNames.CODE_VERIFIER);
+		if (!StringUtils.hasText(codeChallenge)) {
+			if (registeredClient.getClientSettings().isRequireProofKey() || StringUtils.hasText(codeVerifier)) {
+				if (this.logger.isDebugEnabled()) {
+					this.logger.debug(LogMessage.format(
+							"Invalid request: code_challenge is required" + " for registered client '%s'",
+							registeredClient.getId()));
+				}
+				throwInvalidGrant(PkceParameterNames.CODE_CHALLENGE);
+			}
+			else {
+				if (this.logger.isTraceEnabled()) {
+					this.logger.trace("Did not authenticate code verifier since requireProofKey=false");
+				}
+				return false;
+			}
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Validated code verifier parameters");
+		}
+
+		String codeChallengeMethod = (String) authorizationRequest.getAdditionalParameters()
+			.get(PkceParameterNames.CODE_CHALLENGE_METHOD);
+		if (!codeVerifierValid(codeVerifier, codeChallenge, codeChallengeMethod)) {
+			if (this.logger.isDebugEnabled()) {
+				this.logger.debug(LogMessage.format(
+						"Invalid request: code_verifier is missing or invalid" + " for registered client '%s'",
+						registeredClient.getId()));
+			}
+			throwInvalidGrant(PkceParameterNames.CODE_VERIFIER);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Authenticated code verifier");
+		}
+
+		return true;
+	}
+
+	private static boolean authorizationCodeGrant(Map<String, Object> parameters) {
+		if (!AuthorizationGrantType.AUTHORIZATION_CODE.getValue()
+			.equals(parameters.get(OAuth2ParameterNames.GRANT_TYPE))) {
+			return false;
+		}
+		if (!StringUtils.hasText((String) parameters.get(OAuth2ParameterNames.CODE))) {
+			throwInvalidGrant(OAuth2ParameterNames.CODE);
+		}
+		return true;
+	}
+
+	private boolean codeVerifierValid(String codeVerifier, String codeChallenge, String codeChallengeMethod) {
+		if (!StringUtils.hasText(codeVerifier)) {
+			return false;
+		}
+		else if ("S256".equals(codeChallengeMethod)) {
+			try {
+				MessageDigest md = MessageDigest.getInstance("SHA-256");
+				byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
+				String encodedVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
+				return encodedVerifier.equals(codeChallenge);
+			}
+			catch (NoSuchAlgorithmException ex) {
+				// It is unlikely that SHA-256 is not available on the server. If it is
+				// not available,
+				// there will likely be bigger issues as well. We default to SERVER_ERROR.
+				throw new OAuth2AuthenticationException(OAuth2ErrorCodes.SERVER_ERROR);
+			}
+		}
+		return false;
+	}
+
+	private static void throwInvalidGrant(String parameterName) {
+		OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT,
+				"Client authentication failed: " + parameterName, null);
+		throw new OAuth2AuthenticationException(error);
+	}
+
+}

+ 71 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/DPoPProofVerifier.java

@@ -0,0 +1,71 @@
+/*
+ * Copyright 2020-2025 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.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.jwt.DPoPProofContext;
+import org.springframework.security.oauth2.jwt.DPoPProofJwtDecoderFactory;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
+import org.springframework.util.StringUtils;
+
+/**
+ * A verifier for DPoP Proof {@link Jwt}'s.
+ *
+ * @author Joe Grandja
+ * @since 1.5
+ * @see DPoPProofJwtDecoderFactory
+ * @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc9449">RFC 9449
+ * OAuth 2.0 Demonstrating Proof of Possession (DPoP)</a>
+ */
+final class DPoPProofVerifier {
+
+	private static final JwtDecoderFactory<DPoPProofContext> dPoPProofVerifierFactory = new DPoPProofJwtDecoderFactory();
+
+	private DPoPProofVerifier() {
+	}
+
+	static Jwt verifyIfAvailable(OAuth2AuthorizationGrantAuthenticationToken authorizationGrantAuthentication) {
+		String dPoPProof = (String) authorizationGrantAuthentication.getAdditionalParameters().get("dpop_proof");
+		if (!StringUtils.hasText(dPoPProof)) {
+			return null;
+		}
+
+		String method = (String) authorizationGrantAuthentication.getAdditionalParameters().get("dpop_method");
+		String targetUri = (String) authorizationGrantAuthentication.getAdditionalParameters().get("dpop_target_uri");
+
+		Jwt dPoPProofJwt;
+		try {
+			// @formatter:off
+			DPoPProofContext dPoPProofContext = DPoPProofContext.withDPoPProof(dPoPProof)
+					.method(method)
+					.targetUri(targetUri)
+					.build();
+			// @formatter:on
+			JwtDecoder dPoPProofVerifier = dPoPProofVerifierFactory.createDecoder(dPoPProofContext);
+			dPoPProofJwt = dPoPProofVerifier.decode(dPoPProof);
+		}
+		catch (Exception ex) {
+			throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF), ex);
+		}
+
+		return dPoPProofJwt;
+	}
+
+}

+ 172 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/JwtClientAssertionAuthenticationProvider.java

@@ -0,0 +1,172 @@
+/*
+ * 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.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+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.jose.jws.SignatureAlgorithm;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
+import org.springframework.security.oauth2.jwt.JwtException;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link AuthenticationProvider} implementation used for OAuth 2.0 Client
+ * Authentication, which authenticates the {@link Jwt}
+ * {@link OAuth2ParameterNames#CLIENT_ASSERTION client_assertion} parameter.
+ *
+ * @author Rafal Lewczuk
+ * @author Joe Grandja
+ * @since 0.2.3
+ * @see AuthenticationProvider
+ * @see OAuth2ClientAuthenticationToken
+ * @see RegisteredClientRepository
+ * @see OAuth2AuthorizationService
+ * @see JwtClientAssertionDecoderFactory
+ */
+public final class JwtClientAssertionAuthenticationProvider implements AuthenticationProvider {
+
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
+
+	private static final ClientAuthenticationMethod JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD = new ClientAuthenticationMethod(
+			"urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final RegisteredClientRepository registeredClientRepository;
+
+	private final CodeVerifierAuthenticator codeVerifierAuthenticator;
+
+	private JwtDecoderFactory<RegisteredClient> jwtDecoderFactory;
+
+	/**
+	 * Constructs a {@code JwtClientAssertionAuthenticationProvider} using the provided
+	 * parameters.
+	 * @param registeredClientRepository the repository of registered clients
+	 * @param authorizationService the authorization service
+	 */
+	public JwtClientAssertionAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
+			OAuth2AuthorizationService authorizationService) {
+		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		this.registeredClientRepository = registeredClientRepository;
+		this.codeVerifierAuthenticator = new CodeVerifierAuthenticator(authorizationService);
+		this.jwtDecoderFactory = new JwtClientAssertionDecoderFactory();
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) authentication;
+
+		if (!JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod())) {
+			return null;
+		}
+
+		String clientId = clientAuthentication.getPrincipal().toString();
+		RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
+		if (registeredClient == null) {
+			throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved registered client");
+		}
+
+		// @formatter:off
+		if (!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.PRIVATE_KEY_JWT) &&
+				!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
+			throwInvalidClient("authentication_method");
+		}
+		// @formatter:on
+
+		if (clientAuthentication.getCredentials() == null) {
+			throwInvalidClient("credentials");
+		}
+
+		Jwt jwtAssertion = null;
+		JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(registeredClient);
+		try {
+			jwtAssertion = jwtDecoder.decode(clientAuthentication.getCredentials().toString());
+		}
+		catch (JwtException ex) {
+			throwInvalidClient(OAuth2ParameterNames.CLIENT_ASSERTION, ex);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Validated client authentication parameters");
+		}
+
+		// Validate the "code_verifier" parameter for the confidential client, if
+		// available
+		this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);
+
+		// @formatter:off
+		ClientAuthenticationMethod clientAuthenticationMethod =
+				(registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm() instanceof SignatureAlgorithm) ?
+						ClientAuthenticationMethod.PRIVATE_KEY_JWT :
+						ClientAuthenticationMethod.CLIENT_SECRET_JWT;
+		// @formatter:on
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Authenticated client assertion");
+		}
+
+		return new OAuth2ClientAuthenticationToken(registeredClient, clientAuthenticationMethod, jwtAssertion);
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	/**
+	 * Sets the {@link JwtDecoderFactory} that provides a {@link JwtDecoder} for the
+	 * specified {@link RegisteredClient} and is used for authenticating a {@link Jwt}
+	 * Bearer Token during OAuth 2.0 Client Authentication. The default factory is
+	 * {@link JwtClientAssertionDecoderFactory}.
+	 * @param jwtDecoderFactory the {@link JwtDecoderFactory} that provides a
+	 * {@link JwtDecoder} for the specified {@link RegisteredClient}
+	 * @since 0.4.0
+	 */
+	public void setJwtDecoderFactory(JwtDecoderFactory<RegisteredClient> jwtDecoderFactory) {
+		Assert.notNull(jwtDecoderFactory, "jwtDecoderFactory cannot be null");
+		this.jwtDecoderFactory = jwtDecoderFactory;
+	}
+
+	private static void throwInvalidClient(String parameterName) {
+		throwInvalidClient(parameterName, null);
+	}
+
+	private static void throwInvalidClient(String parameterName, Throwable cause) {
+		OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
+				"Client authentication failed: " + parameterName, ERROR_URI);
+		throw new OAuth2AuthenticationException(error, error.toString(), cause);
+	}
+
+}

+ 218 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/JwtClientAssertionDecoderFactory.java

@@ -0,0 +1,218 @@
+/*
+ * Copyright 2020-2025 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.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.crypto.spec.SecretKeySpec;
+
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
+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.OAuth2TokenValidator;
+import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
+import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
+import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtClaimNames;
+import org.springframework.security.oauth2.jwt.JwtClaimValidator;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
+import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+/**
+ * A {@link JwtDecoderFactory factory} that provides a {@link JwtDecoder} for the
+ * specified {@link RegisteredClient} and is used for authenticating a {@link Jwt} Bearer
+ * Token during OAuth 2.0 Client Authentication.
+ *
+ * @author Rafal Lewczuk
+ * @author Joe Grandja
+ * @since 0.4.0
+ * @see JwtDecoderFactory
+ * @see RegisteredClient
+ * @see OAuth2TokenValidator
+ * @see JwtClientAssertionAuthenticationProvider
+ * @see ClientAuthenticationMethod#PRIVATE_KEY_JWT
+ * @see ClientAuthenticationMethod#CLIENT_SECRET_JWT
+ */
+public final class JwtClientAssertionDecoderFactory implements JwtDecoderFactory<RegisteredClient> {
+
+	/**
+	 * The default {@code OAuth2TokenValidator<Jwt>} factory that validates the
+	 * {@link JwtClaimNames#ISS iss}, {@link JwtClaimNames#SUB sub},
+	 * {@link JwtClaimNames#AUD aud}, {@link JwtClaimNames#EXP exp} and
+	 * {@link JwtClaimNames#NBF nbf} claims of the {@link Jwt} for the specified
+	 * {@link RegisteredClient}.
+	 */
+	public static final Function<RegisteredClient, OAuth2TokenValidator<Jwt>> DEFAULT_JWT_VALIDATOR_FACTORY = defaultJwtValidatorFactory();
+
+	private static final String JWT_CLIENT_AUTHENTICATION_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7523#section-3";
+
+	private static final Map<JwsAlgorithm, String> JCA_ALGORITHM_MAPPINGS;
+
+	static {
+		Map<JwsAlgorithm, String> mappings = new HashMap<>();
+		mappings.put(MacAlgorithm.HS256, "HmacSHA256");
+		mappings.put(MacAlgorithm.HS384, "HmacSHA384");
+		mappings.put(MacAlgorithm.HS512, "HmacSHA512");
+		JCA_ALGORITHM_MAPPINGS = Collections.unmodifiableMap(mappings);
+	}
+
+	private static final RestTemplate restTemplate = new RestTemplate();
+
+	static {
+		SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
+		requestFactory.setConnectTimeout(15_000);
+		requestFactory.setReadTimeout(15_000);
+		restTemplate.setRequestFactory(requestFactory);
+	}
+
+	private final Map<String, JwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
+
+	private Function<RegisteredClient, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = DEFAULT_JWT_VALIDATOR_FACTORY;
+
+	@Override
+	public JwtDecoder createDecoder(RegisteredClient registeredClient) {
+		Assert.notNull(registeredClient, "registeredClient cannot be null");
+		return this.jwtDecoders.computeIfAbsent(registeredClient.getId(), (key) -> {
+			NimbusJwtDecoder jwtDecoder = buildDecoder(registeredClient);
+			jwtDecoder.setJwtValidator(this.jwtValidatorFactory.apply(registeredClient));
+			return jwtDecoder;
+		});
+	}
+
+	/**
+	 * Sets the factory that provides an {@link OAuth2TokenValidator} for the specified
+	 * {@link RegisteredClient} and is used by the {@link JwtDecoder}. The default
+	 * {@code OAuth2TokenValidator<Jwt>} factory is
+	 * {@link #DEFAULT_JWT_VALIDATOR_FACTORY}.
+	 * @param jwtValidatorFactory the factory that provides an
+	 * {@link OAuth2TokenValidator} for the specified {@link RegisteredClient}
+	 */
+	public void setJwtValidatorFactory(Function<RegisteredClient, OAuth2TokenValidator<Jwt>> jwtValidatorFactory) {
+		Assert.notNull(jwtValidatorFactory, "jwtValidatorFactory cannot be null");
+		this.jwtValidatorFactory = jwtValidatorFactory;
+	}
+
+	private static NimbusJwtDecoder buildDecoder(RegisteredClient registeredClient) {
+		JwsAlgorithm jwsAlgorithm = registeredClient.getClientSettings()
+			.getTokenEndpointAuthenticationSigningAlgorithm();
+		if (jwsAlgorithm instanceof SignatureAlgorithm) {
+			String jwkSetUrl = registeredClient.getClientSettings().getJwkSetUrl();
+			if (!StringUtils.hasText(jwkSetUrl)) {
+				OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
+						"Failed to find a Signature Verifier for Client: '" + registeredClient.getId()
+								+ "'. Check to ensure you have configured the JWK Set URL.",
+						JWT_CLIENT_AUTHENTICATION_ERROR_URI);
+				throw new OAuth2AuthenticationException(oauth2Error);
+			}
+			return NimbusJwtDecoder.withJwkSetUri(jwkSetUrl)
+				.jwsAlgorithm((SignatureAlgorithm) jwsAlgorithm)
+				.restOperations(restTemplate)
+				.build();
+		}
+		if (jwsAlgorithm instanceof MacAlgorithm) {
+			String clientSecret = registeredClient.getClientSecret();
+			if (!StringUtils.hasText(clientSecret)) {
+				OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
+						"Failed to find a Signature Verifier for Client: '" + registeredClient.getId()
+								+ "'. Check to ensure you have configured the client secret.",
+						JWT_CLIENT_AUTHENTICATION_ERROR_URI);
+				throw new OAuth2AuthenticationException(oauth2Error);
+			}
+			SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8),
+					JCA_ALGORITHM_MAPPINGS.get(jwsAlgorithm));
+			return NimbusJwtDecoder.withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();
+		}
+		OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
+				"Failed to find a Signature Verifier for Client: '" + registeredClient.getId()
+						+ "'. Check to ensure you have configured a valid JWS Algorithm: '" + jwsAlgorithm + "'.",
+				JWT_CLIENT_AUTHENTICATION_ERROR_URI);
+		throw new OAuth2AuthenticationException(oauth2Error);
+	}
+
+	private static Function<RegisteredClient, OAuth2TokenValidator<Jwt>> defaultJwtValidatorFactory() {
+		return (registeredClient) -> {
+			String clientId = registeredClient.getClientId();
+			return new DelegatingOAuth2TokenValidator<>(new JwtClaimValidator<>(JwtClaimNames.ISS, clientId::equals),
+					new JwtClaimValidator<>(JwtClaimNames.SUB, clientId::equals),
+					new JwtClaimValidator<>(JwtClaimNames.AUD, containsAudience()),
+					new JwtClaimValidator<>(JwtClaimNames.EXP, Objects::nonNull), new JwtTimestampValidator());
+		};
+	}
+
+	private static Predicate<List<String>> containsAudience() {
+		return (audienceClaim) -> {
+			if (CollectionUtils.isEmpty(audienceClaim)) {
+				return false;
+			}
+			List<String> audienceList = getAudience();
+			for (String audience : audienceClaim) {
+				if (audienceList.contains(audience)) {
+					return true;
+				}
+			}
+			return false;
+		};
+	}
+
+	private static List<String> getAudience() {
+		AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
+		if (!StringUtils.hasText(authorizationServerContext.getIssuer())) {
+			return Collections.emptyList();
+		}
+
+		AuthorizationServerSettings authorizationServerSettings = authorizationServerContext
+			.getAuthorizationServerSettings();
+		List<String> audience = new ArrayList<>();
+		audience.add(authorizationServerContext.getIssuer());
+		audience.add(asUrl(authorizationServerContext.getIssuer(), authorizationServerSettings.getTokenEndpoint()));
+		audience.add(asUrl(authorizationServerContext.getIssuer(),
+				authorizationServerSettings.getTokenIntrospectionEndpoint()));
+		audience.add(asUrl(authorizationServerContext.getIssuer(),
+				authorizationServerSettings.getTokenRevocationEndpoint()));
+		audience.add(asUrl(authorizationServerContext.getIssuer(),
+				authorizationServerSettings.getPushedAuthorizationRequestEndpoint()));
+		return audience;
+	}
+
+	private static String asUrl(String issuer, String endpoint) {
+		return UriComponentsBuilder.fromUriString(issuer).path(endpoint).build().toUriString();
+	}
+
+}

+ 111 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AccessTokenAuthenticationContext.java

@@ -0,0 +1,111 @@
+/*
+ * Copyright 2020-2025 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.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AccessTokenResponseAuthenticationSuccessHandler;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link OAuth2AuthenticationContext} that holds an
+ * {@link OAuth2AccessTokenAuthenticationToken} and additional information and is used
+ * when customizing the {@link OAuth2AccessTokenResponse}.
+ *
+ * @author Dmitriy Dubson
+ * @since 1.3
+ * @see OAuth2AuthenticationContext
+ * @see OAuth2AccessTokenAuthenticationToken
+ * @see OAuth2AccessTokenResponse
+ * @see OAuth2AccessTokenResponseAuthenticationSuccessHandler#setAccessTokenResponseCustomizer(Consumer)
+ */
+public final class OAuth2AccessTokenAuthenticationContext implements OAuth2AuthenticationContext {
+
+	private final Map<Object, Object> context;
+
+	private OAuth2AccessTokenAuthenticationContext(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 OAuth2AccessTokenResponse.Builder access token response
+	 * builder}.
+	 * @return the {@link OAuth2AccessTokenResponse.Builder}
+	 */
+	public OAuth2AccessTokenResponse.Builder getAccessTokenResponse() {
+		return get(OAuth2AccessTokenResponse.Builder.class);
+	}
+
+	/**
+	 * Constructs a new {@link Builder} with the provided
+	 * {@link OAuth2AccessTokenAuthenticationToken}.
+	 * @param authentication the {@link OAuth2AccessTokenAuthenticationToken}
+	 * @return the {@link Builder}
+	 */
+	public static Builder with(OAuth2AccessTokenAuthenticationToken authentication) {
+		return new Builder(authentication);
+	}
+
+	/**
+	 * A builder for {@link OAuth2AccessTokenAuthenticationContext}.
+	 */
+	public static final class Builder extends AbstractBuilder<OAuth2AccessTokenAuthenticationContext, Builder> {
+
+		private Builder(OAuth2AccessTokenAuthenticationToken authentication) {
+			super(authentication);
+		}
+
+		/**
+		 * Sets the {@link OAuth2AccessTokenResponse.Builder access token response
+		 * builder}.
+		 * @param accessTokenResponse the {@link OAuth2AccessTokenResponse.Builder}
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder accessTokenResponse(OAuth2AccessTokenResponse.Builder accessTokenResponse) {
+			return put(OAuth2AccessTokenResponse.Builder.class, accessTokenResponse);
+		}
+
+		/**
+		 * Builds a new {@link OAuth2AccessTokenAuthenticationContext}.
+		 * @return the {@link OAuth2AccessTokenAuthenticationContext}
+		 */
+		@Override
+		public OAuth2AccessTokenAuthenticationContext build() {
+			Assert.notNull(get(OAuth2AccessTokenResponse.Builder.class), "accessTokenResponse cannot be null");
+			return new OAuth2AccessTokenAuthenticationContext(getContext());
+		}
+
+	}
+
+}

+ 150 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AccessTokenAuthenticationToken.java

@@ -0,0 +1,150 @@
+/*
+ * Copyright 2020-2025 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.io.Serial;
+import java.util.Collections;
+import java.util.Map;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2RefreshToken;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} implementation used when issuing an OAuth 2.0 Access Token
+ * and (optional) Refresh Token.
+ *
+ * @author Joe Grandja
+ * @author Madhu Bhat
+ * @since 0.0.1
+ * @see AbstractAuthenticationToken
+ * @see RegisteredClient
+ * @see OAuth2AccessToken
+ * @see OAuth2RefreshToken
+ * @see OAuth2ClientAuthenticationToken
+ */
+public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthenticationToken {
+
+	@Serial
+	private static final long serialVersionUID = 2773767853287774441L;
+
+	private final RegisteredClient registeredClient;
+
+	private final Authentication clientPrincipal;
+
+	private final OAuth2AccessToken accessToken;
+
+	private final OAuth2RefreshToken refreshToken;
+
+	private final Map<String, Object> additionalParameters;
+
+	/**
+	 * Constructs an {@code OAuth2AccessTokenAuthenticationToken} using the provided
+	 * parameters.
+	 * @param registeredClient the registered client
+	 * @param clientPrincipal the authenticated client principal
+	 * @param accessToken the access token
+	 */
+	public OAuth2AccessTokenAuthenticationToken(RegisteredClient registeredClient, Authentication clientPrincipal,
+			OAuth2AccessToken accessToken) {
+		this(registeredClient, clientPrincipal, accessToken, null);
+	}
+
+	/**
+	 * Constructs an {@code OAuth2AccessTokenAuthenticationToken} using the provided
+	 * parameters.
+	 * @param registeredClient the registered client
+	 * @param clientPrincipal the authenticated client principal
+	 * @param accessToken the access token
+	 * @param refreshToken the refresh token
+	 */
+	public OAuth2AccessTokenAuthenticationToken(RegisteredClient registeredClient, Authentication clientPrincipal,
+			OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken) {
+		this(registeredClient, clientPrincipal, accessToken, refreshToken, Collections.emptyMap());
+	}
+
+	/**
+	 * Constructs an {@code OAuth2AccessTokenAuthenticationToken} using the provided
+	 * parameters.
+	 * @param registeredClient the registered client
+	 * @param clientPrincipal the authenticated client principal
+	 * @param accessToken the access token
+	 * @param refreshToken the refresh token
+	 * @param additionalParameters the additional parameters
+	 */
+	public OAuth2AccessTokenAuthenticationToken(RegisteredClient registeredClient, Authentication clientPrincipal,
+			OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken,
+			Map<String, Object> additionalParameters) {
+		super(Collections.emptyList());
+		Assert.notNull(registeredClient, "registeredClient cannot be null");
+		Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
+		Assert.notNull(accessToken, "accessToken cannot be null");
+		Assert.notNull(additionalParameters, "additionalParameters cannot be null");
+		this.registeredClient = registeredClient;
+		this.clientPrincipal = clientPrincipal;
+		this.accessToken = accessToken;
+		this.refreshToken = refreshToken;
+		this.additionalParameters = additionalParameters;
+	}
+
+	@Override
+	public Object getPrincipal() {
+		return this.clientPrincipal;
+	}
+
+	@Override
+	public Object getCredentials() {
+		return "";
+	}
+
+	/**
+	 * Returns the {@link RegisteredClient registered client}.
+	 * @return the {@link RegisteredClient}
+	 */
+	public RegisteredClient getRegisteredClient() {
+		return this.registeredClient;
+	}
+
+	/**
+	 * Returns the {@link OAuth2AccessToken access token}.
+	 * @return the {@link OAuth2AccessToken}
+	 */
+	public OAuth2AccessToken getAccessToken() {
+		return this.accessToken;
+	}
+
+	/**
+	 * Returns the {@link OAuth2RefreshToken refresh token}.
+	 * @return the {@link OAuth2RefreshToken} or {@code null} if not available
+	 */
+	@Nullable
+	public OAuth2RefreshToken getRefreshToken() {
+		return this.refreshToken;
+	}
+
+	/**
+	 * Returns the additional parameters.
+	 * @return a {@code Map} of the additional parameters, may be empty
+	 */
+	public Map<String, Object> getAdditionalParameters() {
+		return this.additionalParameters;
+	}
+
+}

+ 109 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationContext.java

@@ -0,0 +1,109 @@
+/*
+ * 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.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
+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;
+
+/**
+ * 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 interface OAuth2AuthenticationContext extends Context {
+
+	/**
+	 * Returns the {@link Authentication} associated to the context.
+	 * @param <T> the type of the {@code Authentication}
+	 * @return the {@link Authentication}
+	 */
+	@SuppressWarnings("unchecked")
+	default <T extends Authentication> T getAuthentication() {
+		return (T) get(Authentication.class);
+	}
+
+	/**
+	 * A builder for subclasses of {@link OAuth2AuthenticationContext}.
+	 *
+	 * @param <T> the type of the authentication context
+	 * @param <B> the type of the builder
+	 * @since 0.2.1
+	 */
+	abstract class AbstractBuilder<T extends OAuth2AuthenticationContext, B extends AbstractBuilder<T, B>> {
+
+		private final Map<Object, Object> context = new HashMap<>();
+
+		protected AbstractBuilder(Authentication authentication) {
+			Assert.notNull(authentication, "authentication cannot be null");
+			put(Authentication.class, authentication);
+		}
+
+		/**
+		 * Associates an attribute.
+		 * @param key the key for the attribute
+		 * @param value the value of the attribute
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B put(Object key, Object value) {
+			Assert.notNull(key, "key cannot be null");
+			Assert.notNull(value, "value cannot be null");
+			getContext().put(key, value);
+			return getThis();
+		}
+
+		/**
+		 * A {@code Consumer} of the attributes {@code Map} allowing the ability to add,
+		 * replace, or remove.
+		 * @param contextConsumer a {@link Consumer} of the attributes {@code Map}
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B context(Consumer<Map<Object, Object>> contextConsumer) {
+			contextConsumer.accept(getContext());
+			return getThis();
+		}
+
+		@SuppressWarnings("unchecked")
+		protected <V> V get(Object key) {
+			return (V) getContext().get(key);
+		}
+
+		protected Map<Object, Object> getContext() {
+			return this.context;
+		}
+
+		@SuppressWarnings("unchecked")
+		protected final B getThis() {
+			return (B) this;
+		}
+
+		/**
+		 * Builds a new {@link OAuth2AuthenticationContext}.
+		 * @return the {@link OAuth2AuthenticationContext}
+		 */
+		public abstract T build();
+
+	}
+
+}

+ 80 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationProviderUtils.java

@@ -0,0 +1,80 @@
+/*
+ * Copyright 2020-2025 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.Map;
+
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.ClaimAccessor;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.OAuth2Token;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Utility methods for the OAuth 2.0 {@link AuthenticationProvider}'s.
+ *
+ * @author Joe Grandja
+ * @since 0.0.3
+ */
+final class OAuth2AuthenticationProviderUtils {
+
+	private OAuth2AuthenticationProviderUtils() {
+	}
+
+	static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
+		OAuth2ClientAuthenticationToken clientPrincipal = null;
+		if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
+			clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
+		}
+		if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
+			return clientPrincipal;
+		}
+		throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
+	}
+
+	static <T extends OAuth2Token> OAuth2AccessToken accessToken(OAuth2Authorization.Builder builder, T token,
+			OAuth2TokenContext accessTokenContext) {
+
+		OAuth2AccessToken.TokenType tokenType = OAuth2AccessToken.TokenType.BEARER;
+		if (token instanceof ClaimAccessor claimAccessor) {
+			Map<String, Object> cnfClaims = claimAccessor.getClaimAsMap("cnf");
+			if (!CollectionUtils.isEmpty(cnfClaims) && cnfClaims.containsKey("jkt")) {
+				tokenType = OAuth2AccessToken.TokenType.DPOP;
+			}
+		}
+		OAuth2AccessToken accessToken = new OAuth2AccessToken(tokenType, token.getTokenValue(), token.getIssuedAt(),
+				token.getExpiresAt(), accessTokenContext.getAuthorizedScopes());
+		OAuth2TokenFormat accessTokenFormat = accessTokenContext.getRegisteredClient()
+			.getTokenSettings()
+			.getAccessTokenFormat();
+		builder.token(accessToken, (metadata) -> {
+			if (token instanceof ClaimAccessor claimAccessor) {
+				metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, claimAccessor.getClaims());
+			}
+			metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, false);
+			metadata.put(OAuth2TokenFormat.class.getName(), accessTokenFormat.getValue());
+		});
+
+		return accessToken;
+	}
+
+}

+ 358 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java

@@ -0,0 +1,358 @@
+/*
+ * Copyright 2020-2025 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.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.log.LogMessage;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.session.SessionInformation;
+import org.springframework.security.core.session.SessionRegistry;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+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.OAuth2RefreshToken;
+import org.springframework.security.oauth2.core.OAuth2Token;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.OidcScopes;
+import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Code
+ * Grant.
+ *
+ * @author Joe Grandja
+ * @author Daniel Garnier-Moiroux
+ * @since 0.0.1
+ * @see OAuth2AuthorizationCodeAuthenticationToken
+ * @see OAuth2AccessTokenAuthenticationToken
+ * @see OAuth2AuthorizationCodeRequestAuthenticationProvider
+ * @see OAuth2AuthorizationService
+ * @see OAuth2TokenGenerator
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1">Section 4.1 Authorization
+ * Code Grant</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3">Section 4.1.3 Access
+ * Token Request</a>
+ */
+public final class OAuth2AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
+
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
+
+	private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
+
+	private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final OAuth2AuthorizationService authorizationService;
+
+	private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
+
+	private SessionRegistry sessionRegistry;
+
+	/**
+	 * Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the
+	 * provided parameters.
+	 * @param authorizationService the authorization service
+	 * @param tokenGenerator the token generator
+	 * @since 0.2.3
+	 */
+	public OAuth2AuthorizationCodeAuthenticationProvider(OAuth2AuthorizationService authorizationService,
+			OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
+		this.authorizationService = authorizationService;
+		this.tokenGenerator = tokenGenerator;
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = (OAuth2AuthorizationCodeAuthenticationToken) authentication;
+
+		OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils
+			.getAuthenticatedClientElseThrowInvalidClient(authorizationCodeAuthentication);
+		RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved registered client");
+		}
+
+		OAuth2Authorization authorization = this.authorizationService
+			.findByToken(authorizationCodeAuthentication.getCode(), AUTHORIZATION_CODE_TOKEN_TYPE);
+		if (authorization == null) {
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved authorization with authorization code");
+		}
+
+		OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
+			.getToken(OAuth2AuthorizationCode.class);
+
+		OAuth2AuthorizationRequest authorizationRequest = authorization
+			.getAttribute(OAuth2AuthorizationRequest.class.getName());
+
+		if (!registeredClient.getClientId().equals(authorizationRequest.getClientId())) {
+			if (!authorizationCode.isInvalidated()) {
+				// Invalidate the authorization code given that a different client is
+				// attempting to use it
+				authorization = OAuth2Authorization.from(authorization)
+					.invalidate(authorizationCode.getToken())
+					.build();
+				this.authorizationService.save(authorization);
+				if (this.logger.isWarnEnabled()) {
+					this.logger.warn(LogMessage.format("Invalidated authorization code used by registered client '%s'",
+							registeredClient.getId()));
+				}
+			}
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		if (StringUtils.hasText(authorizationRequest.getRedirectUri())
+				&& !authorizationRequest.getRedirectUri().equals(authorizationCodeAuthentication.getRedirectUri())) {
+			if (this.logger.isDebugEnabled()) {
+				this.logger.debug(LogMessage.format(
+						"Invalid request: redirect_uri does not match" + " for registered client '%s'",
+						registeredClient.getId()));
+			}
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		if (!authorizationCode.isActive()) {
+			if (authorizationCode.isInvalidated()) {
+				OAuth2Authorization.Token<? extends OAuth2Token> token = (authorization.getRefreshToken() != null)
+						? authorization.getRefreshToken() : authorization.getAccessToken();
+				if (token != null) {
+					// Invalidate the access (and refresh) token as the client is
+					// attempting to use the authorization code more than once
+					authorization = OAuth2Authorization.from(authorization).invalidate(token.getToken()).build();
+					this.authorizationService.save(authorization);
+					if (this.logger.isWarnEnabled()) {
+						this.logger.warn(LogMessage.format(
+								"Invalidated authorization token(s) previously issued to registered client '%s'",
+								registeredClient.getId()));
+					}
+				}
+			}
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		// Verify the DPoP Proof (if available)
+		Jwt dPoPProof = DPoPProofVerifier.verifyIfAvailable(authorizationCodeAuthentication);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Validated token request parameters");
+		}
+
+		Authentication principal = authorization.getAttribute(Principal.class.getName());
+
+		// @formatter:off
+		DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
+				.registeredClient(registeredClient)
+				.principal(principal)
+				.authorizationServerContext(AuthorizationServerContextHolder.getContext())
+				.authorization(authorization)
+				.authorizedScopes(authorization.getAuthorizedScopes())
+				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+				.authorizationGrant(authorizationCodeAuthentication);
+		// @formatter:on
+		if (dPoPProof != null) {
+			tokenContextBuilder.put(OAuth2TokenContext.DPOP_PROOF_KEY, dPoPProof);
+		}
+
+		OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization);
+
+		// ----- Access token -----
+		OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
+		OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
+		if (generatedAccessToken == null) {
+			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
+					"The token generator failed to generate the access token.", ERROR_URI);
+			throw new OAuth2AuthenticationException(error);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Generated access token");
+		}
+
+		OAuth2AccessToken accessToken = OAuth2AuthenticationProviderUtils.accessToken(authorizationBuilder,
+				generatedAccessToken, tokenContext);
+
+		// ----- Refresh token -----
+		OAuth2RefreshToken refreshToken = null;
+		// Do not issue refresh token to public client
+		if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) {
+			tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
+			OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
+			if (generatedRefreshToken != null) {
+				if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
+					OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
+							"The token generator failed to generate a valid refresh token.", ERROR_URI);
+					throw new OAuth2AuthenticationException(error);
+				}
+
+				if (this.logger.isTraceEnabled()) {
+					this.logger.trace("Generated refresh token");
+				}
+
+				refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
+				authorizationBuilder.refreshToken(refreshToken);
+			}
+		}
+
+		// ----- ID token -----
+		OidcIdToken idToken;
+		if (authorizationRequest.getScopes().contains(OidcScopes.OPENID)) {
+			SessionInformation sessionInformation = getSessionInformation(principal);
+			if (sessionInformation != null) {
+				try {
+					// Compute (and use) hash for Session ID
+					sessionInformation = new SessionInformation(sessionInformation.getPrincipal(),
+							createHash(sessionInformation.getSessionId()), sessionInformation.getLastRequest());
+				}
+				catch (NoSuchAlgorithmException ex) {
+					OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
+							"Failed to compute hash for Session ID.", ERROR_URI);
+					throw new OAuth2AuthenticationException(error);
+				}
+				tokenContextBuilder.put(SessionInformation.class, sessionInformation);
+			}
+			// @formatter:off
+			tokenContext = tokenContextBuilder
+					.tokenType(ID_TOKEN_TOKEN_TYPE)
+					.authorization(authorizationBuilder.build())	// ID token customizer may need access to the access token and/or refresh token
+					.build();
+			// @formatter:on
+			OAuth2Token generatedIdToken = this.tokenGenerator.generate(tokenContext);
+			if (!(generatedIdToken instanceof Jwt)) {
+				OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
+						"The token generator failed to generate the ID token.", ERROR_URI);
+				throw new OAuth2AuthenticationException(error);
+			}
+
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Generated id token");
+			}
+
+			idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(),
+					generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims());
+			authorizationBuilder.token(idToken,
+					(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()));
+		}
+		else {
+			idToken = null;
+		}
+
+		// Invalidate the authorization code as it can only be used once
+		authorizationBuilder.invalidate(authorizationCode.getToken());
+
+		authorization = authorizationBuilder.build();
+
+		this.authorizationService.save(authorization);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Saved authorization");
+		}
+
+		Map<String, Object> additionalParameters = Collections.emptyMap();
+		if (idToken != null) {
+			additionalParameters = new HashMap<>();
+			additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue());
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Authenticated token request");
+		}
+
+		return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken,
+				additionalParameters);
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	/**
+	 * Sets the {@link SessionRegistry} used to track OpenID Connect sessions.
+	 * @param sessionRegistry the {@link SessionRegistry} used to track OpenID Connect
+	 * sessions
+	 * @since 1.1
+	 */
+	public void setSessionRegistry(SessionRegistry sessionRegistry) {
+		Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
+		this.sessionRegistry = sessionRegistry;
+	}
+
+	private SessionInformation getSessionInformation(Authentication principal) {
+		SessionInformation sessionInformation = null;
+		if (this.sessionRegistry != null) {
+			List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(principal.getPrincipal(), false);
+			if (!CollectionUtils.isEmpty(sessions)) {
+				sessionInformation = sessions.get(0);
+				if (sessions.size() > 1) {
+					// Get the most recent session
+					sessions = new ArrayList<>(sessions);
+					sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
+					sessionInformation = sessions.get(sessions.size() - 1);
+				}
+			}
+		}
+		return sessionInformation;
+	}
+
+	private static String createHash(String value) throws NoSuchAlgorithmException {
+		MessageDigest md = MessageDigest.getInstance("SHA-256");
+		byte[] digest = md.digest(value.getBytes(StandardCharsets.US_ASCII));
+		return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
+	}
+
+}

+ 75 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationToken.java

@@ -0,0 +1,75 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization.authentication;
+
+import java.util.Map;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} implementation used for the OAuth 2.0 Authorization Code
+ * Grant.
+ *
+ * @author Joe Grandja
+ * @author Madhu Bhat
+ * @author Daniel Garnier-Moiroux
+ * @since 0.0.1
+ * @see OAuth2AuthorizationGrantAuthenticationToken
+ * @see OAuth2AuthorizationCodeAuthenticationProvider
+ */
+public class OAuth2AuthorizationCodeAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
+
+	private final String code;
+
+	private final String redirectUri;
+
+	/**
+	 * Constructs an {@code OAuth2AuthorizationCodeAuthenticationToken} using the provided
+	 * parameters.
+	 * @param code the authorization code
+	 * @param clientPrincipal the authenticated client principal
+	 * @param redirectUri the redirect uri
+	 * @param additionalParameters the additional parameters
+	 */
+	public OAuth2AuthorizationCodeAuthenticationToken(String code, Authentication clientPrincipal,
+			@Nullable String redirectUri, @Nullable Map<String, Object> additionalParameters) {
+		super(AuthorizationGrantType.AUTHORIZATION_CODE, clientPrincipal, additionalParameters);
+		Assert.hasText(code, "code cannot be empty");
+		this.code = code;
+		this.redirectUri = redirectUri;
+	}
+
+	/**
+	 * Returns the authorization code.
+	 * @return the authorization code
+	 */
+	public String getCode() {
+		return this.code;
+	}
+
+	/**
+	 * Returns the redirect uri.
+	 * @return the redirect uri
+	 */
+	@Nullable
+	public String getRedirectUri() {
+		return this.redirectUri;
+	}
+
+}

+ 56 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeGenerator.java

@@ -0,0 +1,56 @@
+/*
+ * 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.time.Instant;
+import java.util.Base64;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
+import org.springframework.security.crypto.keygen.StringKeyGenerator;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
+
+/**
+ * An {@link OAuth2TokenGenerator} that generates an {@link OAuth2AuthorizationCode}.
+ *
+ * @author Joe Grandja
+ * @since 0.4.0
+ * @see OAuth2TokenGenerator
+ * @see OAuth2AuthorizationCode
+ * @see OAuth2AuthorizationCodeRequestAuthenticationProvider
+ * @see OAuth2AuthorizationConsentAuthenticationProvider
+ */
+final class OAuth2AuthorizationCodeGenerator implements OAuth2TokenGenerator<OAuth2AuthorizationCode> {
+
+	private final StringKeyGenerator authorizationCodeGenerator = new Base64StringKeyGenerator(
+			Base64.getUrlEncoder().withoutPadding(), 96);
+
+	@Nullable
+	@Override
+	public OAuth2AuthorizationCode generate(OAuth2TokenContext context) {
+		if (context.getTokenType() == null || !OAuth2ParameterNames.CODE.equals(context.getTokenType().getValue())) {
+			return null;
+		}
+		Instant issuedAt = Instant.now();
+		Instant expiresAt = issuedAt
+			.plus(context.getRegisteredClient().getTokenSettings().getAuthorizationCodeTimeToLive());
+		return new OAuth2AuthorizationCode(this.authorizationCodeGenerator.generateKey(), issuedAt, expiresAt);
+	}
+
+}

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

@@ -0,0 +1,153 @@
+/*
+ * Copyright 2020-2025 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 java.util.function.Predicate;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
+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 parameters, as well as,
+ * determining if authorization consent is required.
+ *
+ * @author Joe Grandja
+ * @since 0.4.0
+ * @see OAuth2AuthenticationContext
+ * @see OAuth2AuthorizationCodeRequestAuthenticationToken
+ * @see OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthenticationValidator(Consumer)
+ * @see OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthorizationConsentRequired(Predicate)
+ */
+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);
+	}
+
+	/**
+	 * Returns the {@link OAuth2AuthorizationRequest authorization request}.
+	 * @return the {@link OAuth2AuthorizationRequest}
+	 * @since 1.3
+	 */
+	@Nullable
+	public OAuth2AuthorizationRequest getAuthorizationRequest() {
+		return get(OAuth2AuthorizationRequest.class);
+	}
+
+	/**
+	 * Returns the {@link OAuth2AuthorizationConsent authorization consent}.
+	 * @return the {@link OAuth2AuthorizationConsent}
+	 * @since 1.3
+	 */
+	@Nullable
+	public OAuth2AuthorizationConsent getAuthorizationConsent() {
+		return get(OAuth2AuthorizationConsent.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);
+		}
+
+		/**
+		 * Sets the {@link OAuth2AuthorizationRequest authorization request}.
+		 * @param authorizationRequest the {@link OAuth2AuthorizationRequest}
+		 * @return the {@link Builder} for further configuration
+		 * @since 1.3
+		 */
+		public Builder authorizationRequest(OAuth2AuthorizationRequest authorizationRequest) {
+			return put(OAuth2AuthorizationRequest.class, authorizationRequest);
+		}
+
+		/**
+		 * Sets the {@link OAuth2AuthorizationConsent authorization consent}.
+		 * @param authorizationConsent the {@link OAuth2AuthorizationConsent}
+		 * @return the {@link Builder} for further configuration
+		 * @since 1.3
+		 */
+		public Builder authorizationConsent(OAuth2AuthorizationConsent authorizationConsent) {
+			return put(OAuth2AuthorizationConsent.class, authorizationConsent);
+		}
+
+		/**
+		 * Builds a new {@link OAuth2AuthorizationCodeRequestAuthenticationContext}.
+		 * @return the {@link OAuth2AuthorizationCodeRequestAuthenticationContext}
+		 */
+		@Override
+		public OAuth2AuthorizationCodeRequestAuthenticationContext build() {
+			Assert.notNull(get(RegisteredClient.class), "registeredClient cannot be null");
+			return new OAuth2AuthorizationCodeRequestAuthenticationContext(getContext());
+		}
+
+	}
+
+}

+ 74 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationException.java

@@ -0,0 +1,74 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization.authentication;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+
+/**
+ * This exception is thrown by
+ * {@link OAuth2AuthorizationCodeRequestAuthenticationProvider} when an attempt to
+ * authenticate the OAuth 2.0 Authorization Request (or Consent) fails.
+ *
+ * @author Joe Grandja
+ * @since 0.1.2
+ * @see OAuth2AuthorizationCodeRequestAuthenticationToken
+ * @see OAuth2AuthorizationCodeRequestAuthenticationProvider
+ */
+public class OAuth2AuthorizationCodeRequestAuthenticationException extends OAuth2AuthenticationException {
+
+	private final OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication;
+
+	/**
+	 * Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationException} using
+	 * the provided parameters.
+	 * @param error the {@link OAuth2Error OAuth 2.0 Error}
+	 * @param authorizationCodeRequestAuthentication the {@link Authentication} instance
+	 * of the OAuth 2.0 Authorization Request (or Consent)
+	 */
+	public OAuth2AuthorizationCodeRequestAuthenticationException(OAuth2Error error,
+			@Nullable OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
+		super(error);
+		this.authorizationCodeRequestAuthentication = authorizationCodeRequestAuthentication;
+	}
+
+	/**
+	 * Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationException} using
+	 * the provided parameters.
+	 * @param error the {@link OAuth2Error OAuth 2.0 Error}
+	 * @param cause the root cause
+	 * @param authorizationCodeRequestAuthentication the {@link Authentication} instance
+	 * of the OAuth 2.0 Authorization Request (or Consent)
+	 */
+	public OAuth2AuthorizationCodeRequestAuthenticationException(OAuth2Error error, Throwable cause,
+			@Nullable OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
+		super(error, cause);
+		this.authorizationCodeRequestAuthentication = authorizationCodeRequestAuthentication;
+	}
+
+	/**
+	 * Returns the {@link Authentication} instance of the OAuth 2.0 Authorization Request
+	 * (or Consent), or {@code null} if not available.
+	 * @return the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
+	 */
+	@Nullable
+	public OAuth2AuthorizationCodeRequestAuthenticationToken getAuthorizationCodeRequestAuthentication() {
+		return this.authorizationCodeRequestAuthentication;
+	}
+
+}

+ 508 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java

@@ -0,0 +1,508 @@
+/*
+ * Copyright 2020-2025 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.security.Principal;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.log.LogMessage;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
+import org.springframework.security.crypto.keygen.StringKeyGenerator;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.oidc.OidcScopes;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization
+ * Request used in the Authorization Code Grant.
+ *
+ * @author Joe Grandja
+ * @author Steve Riesenberg
+ * @since 0.1.2
+ * @see OAuth2AuthorizationCodeRequestAuthenticationToken
+ * @see OAuth2AuthorizationCodeRequestAuthenticationValidator
+ * @see OAuth2AuthorizationCodeAuthenticationProvider
+ * @see OAuth2AuthorizationConsentAuthenticationProvider
+ * @see RegisteredClientRepository
+ * @see OAuth2AuthorizationService
+ * @see OAuth2AuthorizationConsentService
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1">Section 4.1.1
+ * Authorization Request</a>
+ * @see <a target="_blank" href=
+ * "https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest">Section 3.1.2.1
+ * Authentication Request</a>
+ */
+public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider {
+
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
+
+	private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
+
+	private static final StringKeyGenerator DEFAULT_STATE_GENERATOR = new Base64StringKeyGenerator(
+			Base64.getUrlEncoder());
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final RegisteredClientRepository registeredClientRepository;
+
+	private final OAuth2AuthorizationService authorizationService;
+
+	private final OAuth2AuthorizationConsentService authorizationConsentService;
+
+	private OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator();
+
+	private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator = new OAuth2AuthorizationCodeRequestAuthenticationValidator();
+
+	private Predicate<OAuth2AuthorizationCodeRequestAuthenticationContext> authorizationConsentRequired = OAuth2AuthorizationCodeRequestAuthenticationProvider::isAuthorizationConsentRequired;
+
+	/**
+	 * Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationProvider} using
+	 * the provided parameters.
+	 * @param registeredClientRepository the repository of registered clients
+	 * @param authorizationService the authorization service
+	 * @param authorizationConsentService the authorization consent service
+	 */
+	public OAuth2AuthorizationCodeRequestAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
+			OAuth2AuthorizationService authorizationService,
+			OAuth2AuthorizationConsentService authorizationConsentService) {
+		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
+		this.registeredClientRepository = registeredClientRepository;
+		this.authorizationService = authorizationService;
+		this.authorizationConsentService = authorizationConsentService;
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
+
+		OAuth2Authorization pushedAuthorization = null;
+		String requestUri = (String) authorizationCodeRequestAuthentication.getAdditionalParameters()
+			.get(OAuth2ParameterNames.REQUEST_URI);
+		if (StringUtils.hasText(requestUri)) {
+			OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = null;
+			try {
+				pushedAuthorizationRequestUri = OAuth2PushedAuthorizationRequestUri.parse(requestUri);
+			}
+			catch (Exception ex) {
+				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REQUEST_URI,
+						authorizationCodeRequestAuthentication, null);
+			}
+
+			pushedAuthorization = this.authorizationService.findByToken(pushedAuthorizationRequestUri.getState(),
+					STATE_TOKEN_TYPE);
+			if (pushedAuthorization == null) {
+				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REQUEST_URI,
+						authorizationCodeRequestAuthentication, null);
+			}
+
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Retrieved authorization with pushed authorization request");
+			}
+
+			OAuth2AuthorizationRequest authorizationRequest = pushedAuthorization
+				.getAttribute(OAuth2AuthorizationRequest.class.getName());
+
+			if (!authorizationCodeRequestAuthentication.getClientId().equals(authorizationRequest.getClientId())) {
+				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,
+						authorizationCodeRequestAuthentication, null);
+			}
+
+			if (Instant.now().isAfter(pushedAuthorizationRequestUri.getExpiresAt())) {
+				// Remove (effectively invalidating) the pushed authorization request
+				this.authorizationService.remove(pushedAuthorization);
+				if (this.logger.isWarnEnabled()) {
+					this.logger
+						.warn(LogMessage.format("Removed expired pushed authorization request for client id '%s'",
+								authorizationRequest.getClientId()));
+				}
+				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REQUEST_URI,
+						authorizationCodeRequestAuthentication, null);
+			}
+
+			authorizationCodeRequestAuthentication = new OAuth2AuthorizationCodeRequestAuthenticationToken(
+					authorizationCodeRequestAuthentication.getAuthorizationUri(), authorizationRequest.getClientId(),
+					(Authentication) authorizationCodeRequestAuthentication.getPrincipal(),
+					authorizationRequest.getRedirectUri(), authorizationRequest.getState(),
+					authorizationRequest.getScopes(), authorizationRequest.getAdditionalParameters());
+		}
+
+		RegisteredClient registeredClient = this.registeredClientRepository
+			.findByClientId(authorizationCodeRequestAuthentication.getClientId());
+		if (registeredClient == null) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,
+					authorizationCodeRequestAuthentication, null);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved registered client");
+		}
+
+		OAuth2AuthorizationCodeRequestAuthenticationContext.Builder authenticationContextBuilder = OAuth2AuthorizationCodeRequestAuthenticationContext
+			.with(authorizationCodeRequestAuthentication)
+			.registeredClient(registeredClient);
+		OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext = authenticationContextBuilder
+			.build();
+
+		// grant_type
+		OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_AUTHORIZATION_GRANT_TYPE_VALIDATOR
+			.accept(authenticationContext);
+
+		// redirect_uri and scope
+		this.authenticationValidator.accept(authenticationContext);
+
+		// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
+		OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_CODE_CHALLENGE_VALIDATOR
+			.accept(authenticationContext);
+
+		// prompt (OPTIONAL for OpenID Connect 1.0 Authentication Request)
+		Set<String> promptValues = Collections.emptySet();
+		if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
+			String prompt = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get("prompt");
+			if (StringUtils.hasText(prompt)) {
+				OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_PROMPT_VALIDATOR
+					.accept(authenticationContext);
+				promptValues = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(prompt, " ")));
+			}
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Validated authorization code request parameters");
+		}
+
+		// ---------------
+		// The request is valid - ensure the resource owner is authenticated
+		// ---------------
+
+		Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();
+		if (!isPrincipalAuthenticated(principal)) {
+			if (promptValues.contains(OidcPrompt.NONE)) {
+				// Return an error instead of displaying the login page (via the
+				// configured AuthenticationEntryPoint)
+				throwError("login_required", "prompt", authorizationCodeRequestAuthentication, registeredClient);
+			}
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Did not authenticate authorization code request since principal not authenticated");
+			}
+			// Return the authorization request as-is where isAuthenticated() is false
+			return authorizationCodeRequestAuthentication;
+		}
+
+		OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
+			.authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri())
+			.clientId(registeredClient.getClientId())
+			.redirectUri(authorizationCodeRequestAuthentication.getRedirectUri())
+			.scopes(authorizationCodeRequestAuthentication.getScopes())
+			.state(authorizationCodeRequestAuthentication.getState())
+			.additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters())
+			.build();
+		authenticationContextBuilder.authorizationRequest(authorizationRequest);
+
+		OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService
+			.findById(registeredClient.getId(), principal.getName());
+		if (currentAuthorizationConsent != null) {
+			authenticationContextBuilder.authorizationConsent(currentAuthorizationConsent);
+		}
+
+		if (this.authorizationConsentRequired.test(authenticationContextBuilder.build())) {
+			if (promptValues.contains(OidcPrompt.NONE)) {
+				// Return an error instead of displaying the consent page
+				throwError("consent_required", "prompt", authorizationCodeRequestAuthentication, registeredClient);
+			}
+
+			String state = DEFAULT_STATE_GENERATOR.generateKey();
+			OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
+				.attribute(OAuth2ParameterNames.STATE, state)
+				.build();
+
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Generated authorization consent state");
+			}
+
+			this.authorizationService.save(authorization);
+
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Saved authorization");
+			}
+
+			if (pushedAuthorization != null) {
+				// Enforce one-time use by removing the pushed authorization request
+				this.authorizationService.remove(pushedAuthorization);
+				if (this.logger.isTraceEnabled()) {
+					this.logger.trace("Removed authorization with pushed authorization request");
+				}
+			}
+
+			Set<String> currentAuthorizedScopes = (currentAuthorizationConsent != null)
+					? currentAuthorizationConsent.getScopes() : null;
+
+			return new OAuth2AuthorizationConsentAuthenticationToken(authorizationRequest.getAuthorizationUri(),
+					registeredClient.getClientId(), principal, state, currentAuthorizedScopes, null);
+		}
+
+		OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(authorizationCodeRequestAuthentication,
+				registeredClient, null, authorizationRequest.getScopes());
+		OAuth2AuthorizationCode authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);
+		if (authorizationCode == null) {
+			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
+					"The token generator failed to generate the authorization code.", ERROR_URI);
+			throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Generated authorization code");
+		}
+
+		OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
+			.authorizedScopes(authorizationRequest.getScopes())
+			.token(authorizationCode)
+			.build();
+		this.authorizationService.save(authorization);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Saved authorization");
+		}
+
+		if (pushedAuthorization != null) {
+			// Enforce one-time use by removing the pushed authorization request
+			this.authorizationService.remove(pushedAuthorization);
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Removed authorization with pushed authorization request");
+			}
+		}
+
+		String redirectUri = authorizationRequest.getRedirectUri();
+		if (!StringUtils.hasText(redirectUri)) {
+			redirectUri = registeredClient.getRedirectUris().iterator().next();
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Authenticated authorization code request");
+		}
+
+		return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationRequest.getAuthorizationUri(),
+				registeredClient.getClientId(), principal, authorizationCode, redirectUri,
+				authorizationRequest.getState(), authorizationRequest.getScopes());
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	/**
+	 * Sets the {@link OAuth2TokenGenerator} that generates the
+	 * {@link OAuth2AuthorizationCode}.
+	 * @param authorizationCodeGenerator the {@link OAuth2TokenGenerator} that generates
+	 * the {@link OAuth2AuthorizationCode}
+	 * @since 0.2.3
+	 */
+	public void setAuthorizationCodeGenerator(
+			OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator) {
+		Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
+		this.authorizationCodeGenerator = authorizationCodeGenerator;
+	}
+
+	/**
+	 * 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>
+	 * <b>NOTE:</b> The authentication validator MUST throw
+	 * {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails.
+	 * @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 setAuthenticationValidator(
+			Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) {
+		Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
+		this.authenticationValidator = authenticationValidator;
+	}
+
+	/**
+	 * Sets the {@code Predicate} used to determine if authorization consent is required.
+	 *
+	 * <p>
+	 * The {@link OAuth2AuthorizationCodeRequestAuthenticationContext} gives the predicate
+	 * access to the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}, as well
+	 * as, the following context attributes:
+	 * <ul>
+	 * <li>The {@link RegisteredClient} associated with the authorization request.</li>
+	 * <li>The {@link OAuth2AuthorizationRequest} containing the authorization request
+	 * parameters.</li>
+	 * <li>The {@link OAuth2AuthorizationConsent} previously granted to the
+	 * {@link RegisteredClient}, or {@code null} if not available.</li>
+	 * </ul>
+	 * @param authorizationConsentRequired the {@code Predicate} used to determine if
+	 * authorization consent is required
+	 * @since 1.3
+	 */
+	public void setAuthorizationConsentRequired(
+			Predicate<OAuth2AuthorizationCodeRequestAuthenticationContext> authorizationConsentRequired) {
+		Assert.notNull(authorizationConsentRequired, "authorizationConsentRequired cannot be null");
+		this.authorizationConsentRequired = authorizationConsentRequired;
+	}
+
+	private static boolean isAuthorizationConsentRequired(
+			OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
+		if (!authenticationContext.getRegisteredClient().getClientSettings().isRequireAuthorizationConsent()) {
+			return false;
+		}
+		// 'openid' scope does not require consent
+		if (authenticationContext.getAuthorizationRequest().getScopes().contains(OidcScopes.OPENID)
+				&& authenticationContext.getAuthorizationRequest().getScopes().size() == 1) {
+			return false;
+		}
+
+		if (authenticationContext.getAuthorizationConsent() != null && authenticationContext.getAuthorizationConsent()
+			.getScopes()
+			.containsAll(authenticationContext.getAuthorizationRequest().getScopes())) {
+			return false;
+		}
+
+		return true;
+	}
+
+	private static OAuth2Authorization.Builder authorizationBuilder(RegisteredClient registeredClient,
+			Authentication principal, OAuth2AuthorizationRequest authorizationRequest) {
+		return OAuth2Authorization.withRegisteredClient(registeredClient)
+			.principalName(principal.getName())
+			.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+			.attribute(Principal.class.getName(), principal)
+			.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest);
+	}
+
+	private static OAuth2TokenContext createAuthorizationCodeTokenContext(
+			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
+			RegisteredClient registeredClient, OAuth2Authorization authorization, Set<String> authorizedScopes) {
+
+		// @formatter:off
+		DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
+				.registeredClient(registeredClient)
+				.principal((Authentication) authorizationCodeRequestAuthentication.getPrincipal())
+				.authorizationServerContext(AuthorizationServerContextHolder.getContext())
+				.tokenType(new OAuth2TokenType(OAuth2ParameterNames.CODE))
+				.authorizedScopes(authorizedScopes)
+				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+				.authorizationGrant(authorizationCodeRequestAuthentication);
+		// @formatter:on
+
+		if (authorization != null) {
+			tokenContextBuilder.authorization(authorization);
+		}
+
+		return tokenContextBuilder.build();
+	}
+
+	private static boolean isPrincipalAuthenticated(Authentication principal) {
+		return principal != null && !AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass())
+				&& principal.isAuthenticated();
+	}
+
+	private static void throwError(String errorCode, String parameterName,
+			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
+			RegisteredClient registeredClient) {
+		throwError(errorCode, parameterName, ERROR_URI, authorizationCodeRequestAuthentication, registeredClient, null);
+	}
+
+	private static void throwError(String errorCode, String parameterName, String errorUri,
+			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
+			RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
+		OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
+		throwError(error, parameterName, authorizationCodeRequestAuthentication, registeredClient,
+				authorizationRequest);
+	}
+
+	private static void throwError(OAuth2Error error, String parameterName,
+			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
+			RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
+
+		String redirectUri = resolveRedirectUri(authorizationCodeRequestAuthentication, authorizationRequest,
+				registeredClient);
+		if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST)
+				&& (parameterName.equals(OAuth2ParameterNames.CLIENT_ID)
+						|| parameterName.equals(OAuth2ParameterNames.STATE))) {
+			redirectUri = null; // Prevent redirects
+		}
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = new OAuth2AuthorizationCodeRequestAuthenticationToken(
+				authorizationCodeRequestAuthentication.getAuthorizationUri(),
+				authorizationCodeRequestAuthentication.getClientId(),
+				(Authentication) authorizationCodeRequestAuthentication.getPrincipal(), redirectUri,
+				authorizationCodeRequestAuthentication.getState(), authorizationCodeRequestAuthentication.getScopes(),
+				authorizationCodeRequestAuthentication.getAdditionalParameters());
+
+		throw new OAuth2AuthorizationCodeRequestAuthenticationException(error,
+				authorizationCodeRequestAuthenticationResult);
+	}
+
+	private static String resolveRedirectUri(
+			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
+			OAuth2AuthorizationRequest authorizationRequest, RegisteredClient registeredClient) {
+
+		if (authorizationCodeRequestAuthentication != null
+				&& StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
+			return authorizationCodeRequestAuthentication.getRedirectUri();
+		}
+		if (authorizationRequest != null && StringUtils.hasText(authorizationRequest.getRedirectUri())) {
+			return authorizationRequest.getRedirectUri();
+		}
+		if (registeredClient != null) {
+			return registeredClient.getRedirectUris().iterator().next();
+		}
+		return null;
+	}
+
+}

+ 93 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationToken.java

@@ -0,0 +1,93 @@
+/*
+ * Copyright 2020-2025 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.io.Serial;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} implementation for the OAuth 2.0 Authorization Request used
+ * in the Authorization Code Grant.
+ *
+ * @author Joe Grandja
+ * @since 0.1.2
+ * @see OAuth2AuthorizationCodeRequestAuthenticationProvider
+ * @see OAuth2AuthorizationConsentAuthenticationProvider
+ */
+public class OAuth2AuthorizationCodeRequestAuthenticationToken
+		extends AbstractOAuth2AuthorizationCodeRequestAuthenticationToken {
+
+	@Serial
+	private static final long serialVersionUID = -1946164725241393094L;
+
+	private final OAuth2AuthorizationCode authorizationCode;
+
+	/**
+	 * Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationToken} using the
+	 * provided parameters.
+	 * @param authorizationUri the authorization URI
+	 * @param clientId the client identifier
+	 * @param principal the {@code Principal} (Resource Owner)
+	 * @param redirectUri the redirect uri
+	 * @param state the state
+	 * @param scopes the requested scope(s)
+	 * @param additionalParameters the additional parameters
+	 * @since 0.4.0
+	 */
+	public OAuth2AuthorizationCodeRequestAuthenticationToken(String authorizationUri, String clientId,
+			Authentication principal, @Nullable String redirectUri, @Nullable String state,
+			@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
+		super(authorizationUri, clientId, principal, redirectUri, state, scopes, additionalParameters);
+		this.authorizationCode = null;
+	}
+
+	/**
+	 * Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationToken} using the
+	 * provided parameters.
+	 * @param authorizationUri the authorization URI
+	 * @param clientId the client identifier
+	 * @param principal the {@code Principal} (Resource Owner)
+	 * @param authorizationCode the {@link OAuth2AuthorizationCode}
+	 * @param redirectUri the redirect uri
+	 * @param state the state
+	 * @param scopes the authorized scope(s)
+	 * @since 0.4.0
+	 */
+	public OAuth2AuthorizationCodeRequestAuthenticationToken(String authorizationUri, String clientId,
+			Authentication principal, OAuth2AuthorizationCode authorizationCode, @Nullable String redirectUri,
+			@Nullable String state, @Nullable Set<String> scopes) {
+		super(authorizationUri, clientId, principal, redirectUri, state, scopes, null);
+		Assert.notNull(authorizationCode, "authorizationCode cannot be null");
+		this.authorizationCode = authorizationCode;
+		setAuthenticated(true);
+	}
+
+	/**
+	 * Returns the {@link OAuth2AuthorizationCode}.
+	 * @return the {@link OAuth2AuthorizationCode}
+	 */
+	@Nullable
+	public OAuth2AuthorizationCode getAuthorizationCode() {
+		return this.authorizationCode;
+	}
+
+}

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

@@ -0,0 +1,312 @@
+/*
+ * Copyright 2020-2025 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.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.log.LogMessage;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+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.endpoint.PkceParameterNames;
+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)
+ * @see OAuth2PushedAuthorizationRequestAuthenticationProvider#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";
+
+	private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1";
+
+	private static final Log LOGGER = LogFactory.getLog(OAuth2AuthorizationCodeRequestAuthenticationValidator.class);
+
+	static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_AUTHORIZATION_GRANT_TYPE_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validateAuthorizationGrantType;
+
+	static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_CODE_CHALLENGE_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validateCodeChallenge;
+
+	static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_PROMPT_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validatePrompt;
+
+	/**
+	 * The default validator for
+	 * {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getRedirectUri()}.
+	 */
+	public static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_REDIRECT_URI_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validateRedirectUri;
+
+	/**
+	 * The default validator for
+	 * {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getScopes()}.
+	 */
+	public static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_SCOPE_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validateScope;
+
+	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 validateAuthorizationGrantType(
+			OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationContext
+			.getAuthentication();
+		RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
+		if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
+			if (LOGGER.isDebugEnabled()) {
+				LOGGER.debug(LogMessage.format(
+						"Invalid request: requested grant_type is not allowed for registered client '%s'",
+						registeredClient.getId()));
+			}
+			throwError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID,
+					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) {
+				if (LOGGER.isDebugEnabled()) {
+					LOGGER.debug(LogMessage.format("Invalid request: redirect_uri is missing or contains a fragment"
+							+ " for registered client '%s'", registeredClient.getId()));
+				}
+				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
+						authorizationCodeRequestAuthentication, registeredClient);
+			}
+
+			if (!isLoopbackAddress(requestedRedirect.getHost())) {
+				// As per
+				// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-22#section-4.1.3
+				// 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-08#section-8.4.2
+				// 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) {
+					if (LOGGER.isDebugEnabled()) {
+						LOGGER.debug(LogMessage.format(
+								"Invalid request: redirect_uri does not match for registered client '%s'",
+								registeredClient.getId()));
+					}
+					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 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)) {
+			if (LOGGER.isDebugEnabled()) {
+				LOGGER.debug(
+						LogMessage.format("Invalid request: requested scope is not allowed for registered client '%s'",
+								registeredClient.getId()));
+			}
+			throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
+					authorizationCodeRequestAuthentication, registeredClient);
+		}
+	}
+
+	private static void validateCodeChallenge(
+			OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationContext
+			.getAuthentication();
+		RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
+
+		// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
+		String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters()
+			.get(PkceParameterNames.CODE_CHALLENGE);
+		if (StringUtils.hasText(codeChallenge)) {
+			String codeChallengeMethod = (String) authorizationCodeRequestAuthentication.getAdditionalParameters()
+				.get(PkceParameterNames.CODE_CHALLENGE_METHOD);
+			if (!StringUtils.hasText(codeChallengeMethod) || !"S256".equals(codeChallengeMethod)) {
+				throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI,
+						authorizationCodeRequestAuthentication, registeredClient);
+			}
+		}
+		else if (registeredClient.getClientSettings().isRequireProofKey()) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI,
+					authorizationCodeRequestAuthentication, registeredClient);
+		}
+	}
+
+	private static void validatePrompt(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationContext
+			.getAuthentication();
+		RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
+
+		// prompt (OPTIONAL for OpenID Connect 1.0 Authentication Request)
+		if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
+			String prompt = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get("prompt");
+			if (StringUtils.hasText(prompt)) {
+				Set<String> promptValues = new HashSet<>(
+						Arrays.asList(StringUtils.delimitedListToStringArray(prompt, " ")));
+				if (promptValues.contains(OidcPrompt.NONE)) {
+					if (promptValues.contains(OidcPrompt.LOGIN) || promptValues.contains(OidcPrompt.CONSENT)
+							|| promptValues.contains(OidcPrompt.SELECT_ACCOUNT)) {
+						throwError(OAuth2ErrorCodes.INVALID_REQUEST, "prompt", authorizationCodeRequestAuthentication,
+								registeredClient);
+					}
+				}
+			}
+		}
+	}
+
+	private static boolean isLoopbackAddress(String host) {
+		if (!StringUtils.hasText(host)) {
+			return false;
+		}
+		// 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) {
+		throwError(errorCode, parameterName, ERROR_URI, authorizationCodeRequestAuthentication, registeredClient);
+	}
+
+	private static void throwError(String errorCode, String parameterName, String errorUri,
+			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
+			RegisteredClient registeredClient) {
+		OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
+		throwError(error, parameterName, authorizationCodeRequestAuthentication, registeredClient);
+	}
+
+	private static void throwError(OAuth2Error error, String parameterName,
+			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
+			RegisteredClient registeredClient) {
+
+		String redirectUri = StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())
+				? authorizationCodeRequestAuthentication.getRedirectUri()
+				: registeredClient.getRedirectUris().iterator().next();
+		if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST)
+				&& parameterName.equals(OAuth2ParameterNames.REDIRECT_URI)) {
+			redirectUri = null; // Prevent redirects
+		}
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = new OAuth2AuthorizationCodeRequestAuthenticationToken(
+				authorizationCodeRequestAuthentication.getAuthorizationUri(),
+				authorizationCodeRequestAuthentication.getClientId(),
+				(Authentication) authorizationCodeRequestAuthentication.getPrincipal(), redirectUri,
+				authorizationCodeRequestAuthentication.getState(), authorizationCodeRequestAuthentication.getScopes(),
+				authorizationCodeRequestAuthentication.getAdditionalParameters());
+		authorizationCodeRequestAuthenticationResult.setAuthenticated(true);
+
+		throw new OAuth2AuthorizationCodeRequestAuthenticationException(error,
+				authorizationCodeRequestAuthenticationResult);
+	}
+
+}

+ 172 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContext.java

@@ -0,0 +1,172 @@
+/*
+ * Copyright 2020-2025 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.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link OAuth2AuthenticationContext} that holds an
+ * {@link OAuth2AuthorizationConsent.Builder} and additional information and is used when
+ * customizing the building of the {@link OAuth2AuthorizationConsent}.
+ *
+ * @author Steve Riesenberg
+ * @author Joe Grandja
+ * @since 0.2.1
+ * @see OAuth2AuthenticationContext
+ * @see OAuth2AuthorizationConsent
+ * @see OAuth2AuthorizationConsentAuthenticationProvider#setAuthorizationConsentCustomizer(Consumer)
+ */
+public final class OAuth2AuthorizationConsentAuthenticationContext implements OAuth2AuthenticationContext {
+
+	private final Map<Object, Object> context;
+
+	private OAuth2AuthorizationConsentAuthenticationContext(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 OAuth2AuthorizationConsent.Builder authorization consent
+	 * builder}.
+	 * @return the {@link OAuth2AuthorizationConsent.Builder}
+	 */
+	public OAuth2AuthorizationConsent.Builder getAuthorizationConsent() {
+		return get(OAuth2AuthorizationConsent.Builder.class);
+	}
+
+	/**
+	 * Returns the {@link RegisteredClient registered client}.
+	 * @return the {@link RegisteredClient}
+	 */
+	public RegisteredClient getRegisteredClient() {
+		return get(RegisteredClient.class);
+	}
+
+	/**
+	 * Returns the {@link OAuth2Authorization authorization}.
+	 * @return the {@link OAuth2Authorization}
+	 */
+	public OAuth2Authorization getAuthorization() {
+		return get(OAuth2Authorization.class);
+	}
+
+	/**
+	 * Returns the {@link OAuth2AuthorizationRequest authorization request}.
+	 * @return the {@link OAuth2AuthorizationRequest}
+	 */
+	public OAuth2AuthorizationRequest getAuthorizationRequest() {
+		return get(OAuth2AuthorizationRequest.class);
+	}
+
+	/**
+	 * Constructs a new {@link Builder} with the provided
+	 * {@link OAuth2AuthorizationConsentAuthenticationToken}.
+	 * @param authentication the {@link OAuth2AuthorizationConsentAuthenticationToken}
+	 * @return the {@link Builder}
+	 */
+	public static Builder with(OAuth2AuthorizationConsentAuthenticationToken authentication) {
+		return new Builder(authentication);
+	}
+
+	/**
+	 * A builder for {@link OAuth2AuthorizationConsentAuthenticationContext}.
+	 */
+	public static final class Builder
+			extends AbstractBuilder<OAuth2AuthorizationConsentAuthenticationContext, Builder> {
+
+		private Builder(OAuth2AuthorizationConsentAuthenticationToken authentication) {
+			super(authentication);
+		}
+
+		/**
+		 * Sets the {@link OAuth2AuthorizationConsent.Builder authorization consent
+		 * builder}.
+		 * @param authorizationConsent the {@link OAuth2AuthorizationConsent.Builder}
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder authorizationConsent(OAuth2AuthorizationConsent.Builder authorizationConsent) {
+			return put(OAuth2AuthorizationConsent.Builder.class, authorizationConsent);
+		}
+
+		/**
+		 * 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);
+		}
+
+		/**
+		 * Sets the {@link OAuth2Authorization authorization}.
+		 * @param authorization the {@link OAuth2Authorization}
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder authorization(OAuth2Authorization authorization) {
+			return put(OAuth2Authorization.class, authorization);
+		}
+
+		/**
+		 * Sets the {@link OAuth2AuthorizationRequest authorization request}.
+		 * @param authorizationRequest the {@link OAuth2AuthorizationRequest}
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder authorizationRequest(OAuth2AuthorizationRequest authorizationRequest) {
+			return put(OAuth2AuthorizationRequest.class, authorizationRequest);
+		}
+
+		/**
+		 * Builds a new {@link OAuth2AuthorizationConsentAuthenticationContext}.
+		 * @return the {@link OAuth2AuthorizationConsentAuthenticationContext}
+		 */
+		@Override
+		public OAuth2AuthorizationConsentAuthenticationContext build() {
+			Assert.notNull(get(OAuth2AuthorizationConsent.Builder.class), "authorizationConsentBuilder cannot be null");
+			Assert.notNull(get(RegisteredClient.class), "registeredClient cannot be null");
+			OAuth2Authorization authorization = get(OAuth2Authorization.class);
+			Assert.notNull(authorization, "authorization cannot be null");
+			if (authorization.getAuthorizationGrantType().equals(AuthorizationGrantType.AUTHORIZATION_CODE)) {
+				Assert.notNull(get(OAuth2AuthorizationRequest.class), "authorizationRequest cannot be null");
+			}
+			return new OAuth2AuthorizationConsentAuthenticationContext(getContext());
+		}
+
+	}
+
+}

+ 380 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationProvider.java

@@ -0,0 +1,380 @@
+/*
+ * 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.HashSet;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.oidc.OidcScopes;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization
+ * Consent used in the Authorization Code Grant.
+ *
+ * @author Joe Grandja
+ * @since 0.4.0
+ * @see OAuth2AuthorizationConsentAuthenticationToken
+ * @see OAuth2AuthorizationConsent
+ * @see OAuth2AuthorizationCodeRequestAuthenticationProvider
+ * @see RegisteredClientRepository
+ * @see OAuth2AuthorizationService
+ * @see OAuth2AuthorizationConsentService
+ */
+public final class OAuth2AuthorizationConsentAuthenticationProvider implements AuthenticationProvider {
+
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
+
+	private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final RegisteredClientRepository registeredClientRepository;
+
+	private final OAuth2AuthorizationService authorizationService;
+
+	private final OAuth2AuthorizationConsentService authorizationConsentService;
+
+	private OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator();
+
+	private Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer;
+
+	/**
+	 * Constructs an {@code OAuth2AuthorizationConsentAuthenticationProvider} using the
+	 * provided parameters.
+	 * @param registeredClientRepository the repository of registered clients
+	 * @param authorizationService the authorization service
+	 * @param authorizationConsentService the authorization consent service
+	 */
+	public OAuth2AuthorizationConsentAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
+			OAuth2AuthorizationService authorizationService,
+			OAuth2AuthorizationConsentService authorizationConsentService) {
+		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
+		this.registeredClientRepository = registeredClientRepository;
+		this.authorizationService = authorizationService;
+		this.authorizationConsentService = authorizationConsentService;
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		if (authentication instanceof OAuth2DeviceAuthorizationConsentAuthenticationToken) {
+			// This is NOT an OAuth 2.0 Authorization Consent for the Authorization Code
+			// Grant,
+			// return null and let OAuth2DeviceAuthorizationConsentAuthenticationProvider
+			// handle it instead
+			return null;
+		}
+
+		OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication = (OAuth2AuthorizationConsentAuthenticationToken) authentication;
+
+		OAuth2Authorization authorization = this.authorizationService
+			.findByToken(authorizationConsentAuthentication.getState(), STATE_TOKEN_TYPE);
+		if (authorization == null) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE, authorizationConsentAuthentication,
+					null, null);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved authorization with authorization consent state");
+		}
+
+		// The 'in-flight' authorization must be associated to the current principal
+		Authentication principal = (Authentication) authorizationConsentAuthentication.getPrincipal();
+		if (!isPrincipalAuthenticated(principal) || !principal.getName().equals(authorization.getPrincipalName())) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE, authorizationConsentAuthentication,
+					null, null);
+		}
+
+		RegisteredClient registeredClient = this.registeredClientRepository
+			.findByClientId(authorizationConsentAuthentication.getClientId());
+		if (registeredClient == null || !registeredClient.getId().equals(authorization.getRegisteredClientId())) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,
+					authorizationConsentAuthentication, registeredClient, null);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved registered client");
+		}
+
+		OAuth2AuthorizationRequest authorizationRequest = authorization
+			.getAttribute(OAuth2AuthorizationRequest.class.getName());
+		Set<String> requestedScopes = authorizationRequest.getScopes();
+		Set<String> authorizedScopes = new HashSet<>(authorizationConsentAuthentication.getScopes());
+		if (!requestedScopes.containsAll(authorizedScopes)) {
+			throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE, authorizationConsentAuthentication,
+					registeredClient, authorizationRequest);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Validated authorization consent request parameters");
+		}
+
+		OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService
+			.findById(authorization.getRegisteredClientId(), authorization.getPrincipalName());
+		Set<String> currentAuthorizedScopes = (currentAuthorizationConsent != null)
+				? currentAuthorizationConsent.getScopes() : Collections.emptySet();
+
+		if (!currentAuthorizedScopes.isEmpty()) {
+			for (String requestedScope : requestedScopes) {
+				if (currentAuthorizedScopes.contains(requestedScope)) {
+					authorizedScopes.add(requestedScope);
+				}
+			}
+		}
+
+		if (!authorizedScopes.isEmpty() && requestedScopes.contains(OidcScopes.OPENID)) {
+			// 'openid' scope is auto-approved as it does not require consent
+			authorizedScopes.add(OidcScopes.OPENID);
+		}
+
+		OAuth2AuthorizationConsent.Builder authorizationConsentBuilder;
+		if (currentAuthorizationConsent != null) {
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Retrieved existing authorization consent");
+			}
+			authorizationConsentBuilder = OAuth2AuthorizationConsent.from(currentAuthorizationConsent);
+		}
+		else {
+			authorizationConsentBuilder = OAuth2AuthorizationConsent.withId(authorization.getRegisteredClientId(),
+					authorization.getPrincipalName());
+		}
+		authorizedScopes.forEach(authorizationConsentBuilder::scope);
+
+		if (this.authorizationConsentCustomizer != null) {
+			// @formatter:off
+			OAuth2AuthorizationConsentAuthenticationContext authorizationConsentAuthenticationContext =
+					OAuth2AuthorizationConsentAuthenticationContext.with(authorizationConsentAuthentication)
+							.authorizationConsent(authorizationConsentBuilder)
+							.registeredClient(registeredClient)
+							.authorization(authorization)
+							.authorizationRequest(authorizationRequest)
+							.build();
+			// @formatter:on
+			this.authorizationConsentCustomizer.accept(authorizationConsentAuthenticationContext);
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Customized authorization consent");
+			}
+		}
+
+		Set<GrantedAuthority> authorities = new HashSet<>();
+		authorizationConsentBuilder.authorities(authorities::addAll);
+
+		if (authorities.isEmpty()) {
+			// Authorization consent denied (or revoked)
+			if (currentAuthorizationConsent != null) {
+				this.authorizationConsentService.remove(currentAuthorizationConsent);
+				if (this.logger.isTraceEnabled()) {
+					this.logger.trace("Revoked authorization consent");
+				}
+			}
+			this.authorizationService.remove(authorization);
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Removed authorization");
+			}
+			throwError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID,
+					authorizationConsentAuthentication, registeredClient, authorizationRequest);
+		}
+
+		OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();
+		if (!authorizationConsent.equals(currentAuthorizationConsent)) {
+			this.authorizationConsentService.save(authorizationConsent);
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Saved authorization consent");
+			}
+		}
+
+		OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(authorizationConsentAuthentication,
+				registeredClient, authorization, authorizedScopes);
+		OAuth2AuthorizationCode authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);
+		if (authorizationCode == null) {
+			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
+					"The token generator failed to generate the authorization code.", ERROR_URI);
+			throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Generated authorization code");
+		}
+
+		OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
+			.authorizedScopes(authorizedScopes)
+			.token(authorizationCode)
+			.attributes((attrs) -> attrs.remove(OAuth2ParameterNames.STATE))
+			.build();
+		this.authorizationService.save(updatedAuthorization);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Saved authorization");
+		}
+
+		String redirectUri = authorizationRequest.getRedirectUri();
+		if (!StringUtils.hasText(redirectUri)) {
+			redirectUri = registeredClient.getRedirectUris().iterator().next();
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Authenticated authorization consent request");
+		}
+
+		return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationRequest.getAuthorizationUri(),
+				registeredClient.getClientId(), principal, authorizationCode, redirectUri,
+				authorizationRequest.getState(), authorizedScopes);
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2AuthorizationConsentAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	/**
+	 * Sets the {@link OAuth2TokenGenerator} that generates the
+	 * {@link OAuth2AuthorizationCode}.
+	 * @param authorizationCodeGenerator the {@link OAuth2TokenGenerator} that generates
+	 * the {@link OAuth2AuthorizationCode}
+	 */
+	public void setAuthorizationCodeGenerator(
+			OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator) {
+		Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
+		this.authorizationCodeGenerator = authorizationCodeGenerator;
+	}
+
+	/**
+	 * Sets the {@code Consumer} providing access to the
+	 * {@link OAuth2AuthorizationConsentAuthenticationContext} containing an
+	 * {@link OAuth2AuthorizationConsent.Builder} and additional context information.
+	 *
+	 * <p>
+	 * The following context attributes are available:
+	 * <ul>
+	 * <li>The {@link OAuth2AuthorizationConsent.Builder} used to build the authorization
+	 * consent prior to
+	 * {@link OAuth2AuthorizationConsentService#save(OAuth2AuthorizationConsent)}.</li>
+	 * <li>The {@link Authentication} of type
+	 * {@link OAuth2AuthorizationConsentAuthenticationToken}.</li>
+	 * <li>The {@link RegisteredClient} associated with the authorization request.</li>
+	 * <li>The {@link OAuth2Authorization} associated with the state token presented in
+	 * the authorization consent request.</li>
+	 * <li>The {@link OAuth2AuthorizationRequest} associated with the authorization
+	 * consent request.</li>
+	 * </ul>
+	 * @param authorizationConsentCustomizer the {@code Consumer} providing access to the
+	 * {@link OAuth2AuthorizationConsentAuthenticationContext} containing an
+	 * {@link OAuth2AuthorizationConsent.Builder}
+	 */
+	public void setAuthorizationConsentCustomizer(
+			Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer) {
+		Assert.notNull(authorizationConsentCustomizer, "authorizationConsentCustomizer cannot be null");
+		this.authorizationConsentCustomizer = authorizationConsentCustomizer;
+	}
+
+	private static OAuth2TokenContext createAuthorizationCodeTokenContext(
+			OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication,
+			RegisteredClient registeredClient, OAuth2Authorization authorization, Set<String> authorizedScopes) {
+
+		// @formatter:off
+		return DefaultOAuth2TokenContext.builder()
+				.registeredClient(registeredClient)
+				.principal((Authentication) authorizationConsentAuthentication.getPrincipal())
+				.authorization(authorization)
+				.authorizationServerContext(AuthorizationServerContextHolder.getContext())
+				.tokenType(new OAuth2TokenType(OAuth2ParameterNames.CODE))
+				.authorizedScopes(authorizedScopes)
+				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+				.authorizationGrant(authorizationConsentAuthentication)
+				.build();
+		// @formatter:on
+	}
+
+	private static boolean isPrincipalAuthenticated(Authentication principal) {
+		return principal != null && !AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass())
+				&& principal.isAuthenticated();
+	}
+
+	private static void throwError(String errorCode, String parameterName,
+			OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication,
+			RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
+		OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, ERROR_URI);
+		throwError(error, parameterName, authorizationConsentAuthentication, registeredClient, authorizationRequest);
+	}
+
+	private static void throwError(OAuth2Error error, String parameterName,
+			OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication,
+			RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
+
+		String redirectUri = resolveRedirectUri(authorizationRequest, registeredClient);
+		if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST)
+				&& (parameterName.equals(OAuth2ParameterNames.CLIENT_ID)
+						|| parameterName.equals(OAuth2ParameterNames.STATE))) {
+			redirectUri = null; // Prevent redirects
+		}
+
+		String state = (authorizationRequest != null) ? authorizationRequest.getState()
+				: authorizationConsentAuthentication.getState();
+		Set<String> requestedScopes = (authorizationRequest != null) ? authorizationRequest.getScopes()
+				: authorizationConsentAuthentication.getScopes();
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = new OAuth2AuthorizationCodeRequestAuthenticationToken(
+				authorizationConsentAuthentication.getAuthorizationUri(),
+				authorizationConsentAuthentication.getClientId(),
+				(Authentication) authorizationConsentAuthentication.getPrincipal(), redirectUri, state, requestedScopes,
+				null);
+
+		throw new OAuth2AuthorizationCodeRequestAuthenticationException(error,
+				authorizationCodeRequestAuthenticationResult);
+	}
+
+	private static String resolveRedirectUri(OAuth2AuthorizationRequest authorizationRequest,
+			RegisteredClient registeredClient) {
+		if (authorizationRequest != null && StringUtils.hasText(authorizationRequest.getRedirectUri())) {
+			return authorizationRequest.getRedirectUri();
+		}
+		if (registeredClient != null) {
+			return registeredClient.getRedirectUris().iterator().next();
+		}
+		return null;
+	}
+
+}

+ 135 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationToken.java

@@ -0,0 +1,135 @@
+/*
+ * Copyright 2020-2025 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.io.Serial;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} implementation for the OAuth 2.0 Authorization Consent used
+ * in the Authorization Code Grant.
+ *
+ * @author Joe Grandja
+ * @since 0.4.0
+ * @see OAuth2AuthorizationConsentAuthenticationProvider
+ * @see OAuth2AuthorizationCodeRequestAuthenticationProvider
+ */
+public class OAuth2AuthorizationConsentAuthenticationToken extends AbstractAuthenticationToken {
+
+	@Serial
+	private static final long serialVersionUID = -2111287271882598208L;
+
+	private final String authorizationUri;
+
+	private final String clientId;
+
+	private final Authentication principal;
+
+	private final String state;
+
+	private final Set<String> scopes;
+
+	private final Map<String, Object> additionalParameters;
+
+	/**
+	 * Constructs an {@code OAuth2AuthorizationConsentAuthenticationToken} using the
+	 * provided parameters.
+	 * @param authorizationUri the authorization URI
+	 * @param clientId the client identifier
+	 * @param principal the {@code Principal} (Resource Owner)
+	 * @param state the state
+	 * @param scopes the requested (or authorized) scope(s)
+	 * @param additionalParameters the additional parameters
+	 */
+	public OAuth2AuthorizationConsentAuthenticationToken(String authorizationUri, String clientId,
+			Authentication principal, String state, @Nullable Set<String> scopes,
+			@Nullable Map<String, Object> additionalParameters) {
+		super(Collections.emptyList());
+		Assert.hasText(authorizationUri, "authorizationUri cannot be empty");
+		Assert.hasText(clientId, "clientId cannot be empty");
+		Assert.notNull(principal, "principal cannot be null");
+		Assert.hasText(state, "state cannot be empty");
+		this.authorizationUri = authorizationUri;
+		this.clientId = clientId;
+		this.principal = principal;
+		this.state = state;
+		this.scopes = Collections.unmodifiableSet((scopes != null) ? new HashSet<>(scopes) : Collections.emptySet());
+		this.additionalParameters = Collections.unmodifiableMap(
+				(additionalParameters != null) ? new HashMap<>(additionalParameters) : Collections.emptyMap());
+		setAuthenticated(true);
+	}
+
+	@Override
+	public Object getPrincipal() {
+		return this.principal;
+	}
+
+	@Override
+	public Object getCredentials() {
+		return "";
+	}
+
+	/**
+	 * Returns the authorization URI.
+	 * @return the authorization URI
+	 */
+	public String getAuthorizationUri() {
+		return this.authorizationUri;
+	}
+
+	/**
+	 * Returns the client identifier.
+	 * @return the client identifier
+	 */
+	public String getClientId() {
+		return this.clientId;
+	}
+
+	/**
+	 * Returns the state.
+	 * @return the state
+	 */
+	public String getState() {
+		return this.state;
+	}
+
+	/**
+	 * Returns the requested (or authorized) scope(s).
+	 * @return the requested (or authorized) scope(s), or an empty {@code Set} if not
+	 * available
+	 */
+	public Set<String> getScopes() {
+		return this.scopes;
+	}
+
+	/**
+	 * Returns the additional parameters.
+	 * @return the additional parameters, or an empty {@code Map} if not available
+	 */
+	public Map<String, Object> getAdditionalParameters() {
+		return this.additionalParameters;
+	}
+
+}

+ 95 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationGrantAuthenticationToken.java

@@ -0,0 +1,95 @@
+/*
+ * Copyright 2020-2025 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.io.Serial;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.util.Assert;
+
+/**
+ * Base implementation of an {@link Authentication} representing an OAuth 2.0
+ * Authorization Grant.
+ *
+ * @author Joe Grandja
+ * @since 0.1.0
+ * @see AbstractAuthenticationToken
+ * @see AuthorizationGrantType
+ * @see OAuth2ClientAuthenticationToken
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section
+ * 1.3 Authorization Grant</a>
+ */
+public class OAuth2AuthorizationGrantAuthenticationToken extends AbstractAuthenticationToken {
+
+	@Serial
+	private static final long serialVersionUID = -1715946281123199051L;
+
+	private final AuthorizationGrantType authorizationGrantType;
+
+	private final Authentication clientPrincipal;
+
+	private final Map<String, Object> additionalParameters;
+
+	/**
+	 * Sub-class constructor.
+	 * @param authorizationGrantType the authorization grant type
+	 * @param clientPrincipal the authenticated client principal
+	 * @param additionalParameters the additional parameters
+	 */
+	protected OAuth2AuthorizationGrantAuthenticationToken(AuthorizationGrantType authorizationGrantType,
+			Authentication clientPrincipal, @Nullable Map<String, Object> additionalParameters) {
+		super(Collections.emptyList());
+		Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null");
+		Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
+		this.authorizationGrantType = authorizationGrantType;
+		this.clientPrincipal = clientPrincipal;
+		this.additionalParameters = Collections.unmodifiableMap(
+				(additionalParameters != null) ? new HashMap<>(additionalParameters) : Collections.emptyMap());
+	}
+
+	/**
+	 * Returns the authorization grant type.
+	 * @return the authorization grant type
+	 */
+	public AuthorizationGrantType getGrantType() {
+		return this.authorizationGrantType;
+	}
+
+	@Override
+	public Object getPrincipal() {
+		return this.clientPrincipal;
+	}
+
+	@Override
+	public Object getCredentials() {
+		return "";
+	}
+
+	/**
+	 * Returns the additional parameters.
+	 * @return the additional parameters
+	 */
+	public Map<String, Object> getAdditionalParameters() {
+		return this.additionalParameters;
+	}
+
+}

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

@@ -0,0 +1,107 @@
+/*
+ * Copyright 2020-2025 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 OAuth2ClientAuthenticationToken} and additional information and is used when
+ * validating an OAuth 2.0 Client Authentication.
+ *
+ * @author Joe Grandja
+ * @since 1.3
+ * @see OAuth2AuthenticationContext
+ * @see OAuth2ClientAuthenticationToken
+ * @see X509ClientCertificateAuthenticationProvider#setCertificateVerifier(Consumer)
+ */
+public final class OAuth2ClientAuthenticationContext implements OAuth2AuthenticationContext {
+
+	private final Map<Object, Object> context;
+
+	private OAuth2ClientAuthenticationContext(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 OAuth2ClientAuthenticationToken}.
+	 * @param authentication the {@link OAuth2ClientAuthenticationToken}
+	 * @return the {@link Builder}
+	 */
+	public static Builder with(OAuth2ClientAuthenticationToken authentication) {
+		return new Builder(authentication);
+	}
+
+	/**
+	 * A builder for {@link OAuth2ClientAuthenticationContext}.
+	 */
+	public static final class Builder extends AbstractBuilder<OAuth2ClientAuthenticationContext, Builder> {
+
+		private Builder(OAuth2ClientAuthenticationToken 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 OAuth2ClientAuthenticationContext}.
+		 * @return the {@link OAuth2ClientAuthenticationContext}
+		 */
+		@Override
+		public OAuth2ClientAuthenticationContext build() {
+			Assert.notNull(get(RegisteredClient.class), "registeredClient cannot be null");
+			return new OAuth2ClientAuthenticationContext(getContext());
+		}
+
+	}
+
+}

+ 139 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationToken.java

@@ -0,0 +1,139 @@
+/*
+ * Copyright 2020-2025 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.io.Serial;
+import java.util.Collections;
+import java.util.Map;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.Transient;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} implementation used for OAuth 2.0 Client Authentication.
+ *
+ * @author Joe Grandja
+ * @author Patryk Kostrzewa
+ * @author Anoop Garlapati
+ * @since 0.0.1
+ * @see AbstractAuthenticationToken
+ * @see RegisteredClient
+ * @see JwtClientAssertionAuthenticationProvider
+ * @see ClientSecretAuthenticationProvider
+ * @see PublicClientAuthenticationProvider
+ */
+@Transient
+public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken {
+
+	@Serial
+	private static final long serialVersionUID = -7150784632941221304L;
+
+	private final String clientId;
+
+	private final RegisteredClient registeredClient;
+
+	private final ClientAuthenticationMethod clientAuthenticationMethod;
+
+	private final Object credentials;
+
+	private final Map<String, Object> additionalParameters;
+
+	/**
+	 * Constructs an {@code OAuth2ClientAuthenticationToken} using the provided
+	 * parameters.
+	 * @param clientId the client identifier
+	 * @param clientAuthenticationMethod the authentication method used by the client
+	 * @param credentials the client credentials
+	 * @param additionalParameters the additional parameters
+	 */
+	public OAuth2ClientAuthenticationToken(String clientId, ClientAuthenticationMethod clientAuthenticationMethod,
+			@Nullable Object credentials, @Nullable Map<String, Object> additionalParameters) {
+		super(Collections.emptyList());
+		Assert.hasText(clientId, "clientId cannot be empty");
+		Assert.notNull(clientAuthenticationMethod, "clientAuthenticationMethod cannot be null");
+		this.clientId = clientId;
+		this.registeredClient = null;
+		this.clientAuthenticationMethod = clientAuthenticationMethod;
+		this.credentials = credentials;
+		this.additionalParameters = Collections
+			.unmodifiableMap((additionalParameters != null) ? additionalParameters : Collections.emptyMap());
+	}
+
+	/**
+	 * Constructs an {@code OAuth2ClientAuthenticationToken} using the provided
+	 * parameters.
+	 * @param registeredClient the authenticated registered client
+	 * @param clientAuthenticationMethod the authentication method used by the client
+	 * @param credentials the client credentials
+	 */
+	public OAuth2ClientAuthenticationToken(RegisteredClient registeredClient,
+			ClientAuthenticationMethod clientAuthenticationMethod, @Nullable Object credentials) {
+		super(Collections.emptyList());
+		Assert.notNull(registeredClient, "registeredClient cannot be null");
+		Assert.notNull(clientAuthenticationMethod, "clientAuthenticationMethod cannot be null");
+		this.clientId = registeredClient.getClientId();
+		this.registeredClient = registeredClient;
+		this.clientAuthenticationMethod = clientAuthenticationMethod;
+		this.credentials = credentials;
+		this.additionalParameters = Collections.emptyMap();
+		setAuthenticated(true);
+	}
+
+	@Override
+	public Object getPrincipal() {
+		return this.clientId;
+	}
+
+	@Nullable
+	@Override
+	public Object getCredentials() {
+		return this.credentials;
+	}
+
+	/**
+	 * Returns the authenticated {@link RegisteredClient registered client}, or
+	 * {@code null} if not authenticated.
+	 * @return the authenticated {@link RegisteredClient}, or {@code null} if not
+	 * authenticated
+	 */
+	@Nullable
+	public RegisteredClient getRegisteredClient() {
+		return this.registeredClient;
+	}
+
+	/**
+	 * Returns the {@link ClientAuthenticationMethod authentication method} used by the
+	 * client.
+	 * @return the {@link ClientAuthenticationMethod} used by the client
+	 */
+	public ClientAuthenticationMethod getClientAuthenticationMethod() {
+		return this.clientAuthenticationMethod;
+	}
+
+	/**
+	 * Returns the additional parameters.
+	 * @return the additional parameters
+	 */
+	public Map<String, Object> getAdditionalParameters() {
+		return this.additionalParameters;
+	}
+
+}

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

@@ -0,0 +1,107 @@
+/*
+ * Copyright 2020-2025 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 OAuth2ClientCredentialsAuthenticationToken} and additional information and is
+ * used when validating the OAuth 2.0 Client Credentials Grant Request.
+ *
+ * @author Adam Pilling
+ * @since 1.3
+ * @see OAuth2AuthenticationContext
+ * @see OAuth2ClientCredentialsAuthenticationToken
+ * @see OAuth2ClientCredentialsAuthenticationProvider#setAuthenticationValidator(Consumer)
+ */
+public final class OAuth2ClientCredentialsAuthenticationContext implements OAuth2AuthenticationContext {
+
+	private final Map<Object, Object> context;
+
+	private OAuth2ClientCredentialsAuthenticationContext(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 OAuth2ClientCredentialsAuthenticationToken}.
+	 * @param authentication the {@link OAuth2ClientCredentialsAuthenticationToken}
+	 * @return the {@link Builder}
+	 */
+	public static Builder with(OAuth2ClientCredentialsAuthenticationToken authentication) {
+		return new Builder(authentication);
+	}
+
+	/**
+	 * A builder for {@link OAuth2ClientCredentialsAuthenticationContext}.
+	 */
+	public static final class Builder extends AbstractBuilder<OAuth2ClientCredentialsAuthenticationContext, Builder> {
+
+		private Builder(OAuth2ClientCredentialsAuthenticationToken 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 OAuth2ClientCredentialsAuthenticationContext}.
+		 * @return the {@link OAuth2ClientCredentialsAuthenticationContext}
+		 */
+		@Override
+		public OAuth2ClientCredentialsAuthenticationContext build() {
+			Assert.notNull(get(RegisteredClient.class), "registeredClient cannot be null");
+			return new OAuth2ClientCredentialsAuthenticationContext(getContext());
+		}
+
+	}
+
+}

+ 202 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java

@@ -0,0 +1,202 @@
+/*
+ * Copyright 2020-2025 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.LinkedHashSet;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.log.LogMessage;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+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.OAuth2Token;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Client Credentials
+ * Grant.
+ *
+ * @author Alexey Nesterov
+ * @author Joe Grandja
+ * @since 0.0.1
+ * @see OAuth2ClientCredentialsAuthenticationToken
+ * @see OAuth2AccessTokenAuthenticationToken
+ * @see OAuth2AuthorizationService
+ * @see OAuth2TokenGenerator
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc6749#section-4.4">Section 4.4 Client
+ * Credentials Grant</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc6749#section-4.4.2">Section 4.4.2 Access
+ * Token Request</a>
+ */
+public final class OAuth2ClientCredentialsAuthenticationProvider implements AuthenticationProvider {
+
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final OAuth2AuthorizationService authorizationService;
+
+	private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
+
+	private Consumer<OAuth2ClientCredentialsAuthenticationContext> authenticationValidator = new OAuth2ClientCredentialsAuthenticationValidator();
+
+	/**
+	 * Constructs an {@code OAuth2ClientCredentialsAuthenticationProvider} using the
+	 * provided parameters.
+	 * @param authorizationService the authorization service
+	 * @param tokenGenerator the token generator
+	 * @since 0.2.3
+	 */
+	public OAuth2ClientCredentialsAuthenticationProvider(OAuth2AuthorizationService authorizationService,
+			OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
+		this.authorizationService = authorizationService;
+		this.tokenGenerator = tokenGenerator;
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2ClientCredentialsAuthenticationToken clientCredentialsAuthentication = (OAuth2ClientCredentialsAuthenticationToken) authentication;
+
+		OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils
+			.getAuthenticatedClientElseThrowInvalidClient(clientCredentialsAuthentication);
+		RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved registered client");
+		}
+
+		if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.CLIENT_CREDENTIALS)) {
+			if (this.logger.isDebugEnabled()) {
+				this.logger.debug(LogMessage.format(
+						"Invalid request: requested grant_type is not allowed" + " for registered client '%s'",
+						registeredClient.getId()));
+			}
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
+		}
+
+		OAuth2ClientCredentialsAuthenticationContext authenticationContext = OAuth2ClientCredentialsAuthenticationContext
+			.with(clientCredentialsAuthentication)
+			.registeredClient(registeredClient)
+			.build();
+		this.authenticationValidator.accept(authenticationContext);
+
+		Set<String> authorizedScopes = new LinkedHashSet<>(clientCredentialsAuthentication.getScopes());
+
+		// Verify the DPoP Proof (if available)
+		Jwt dPoPProof = DPoPProofVerifier.verifyIfAvailable(clientCredentialsAuthentication);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Validated token request parameters");
+		}
+
+		// @formatter:off
+		DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
+				.registeredClient(registeredClient)
+				.principal(clientPrincipal)
+				.authorizationServerContext(AuthorizationServerContextHolder.getContext())
+				.authorizedScopes(authorizedScopes)
+				.tokenType(OAuth2TokenType.ACCESS_TOKEN)
+				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
+				.authorizationGrant(clientCredentialsAuthentication);
+		// @formatter:on
+		if (dPoPProof != null) {
+			tokenContextBuilder.put(OAuth2TokenContext.DPOP_PROOF_KEY, dPoPProof);
+		}
+		OAuth2TokenContext tokenContext = tokenContextBuilder.build();
+
+		OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
+		if (generatedAccessToken == null) {
+			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
+					"The token generator failed to generate the access token.", ERROR_URI);
+			throw new OAuth2AuthenticationException(error);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Generated access token");
+		}
+
+		// @formatter:off
+		OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
+				.principalName(clientPrincipal.getName())
+				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
+				.authorizedScopes(authorizedScopes);
+		// @formatter:on
+
+		OAuth2AccessToken accessToken = OAuth2AuthenticationProviderUtils.accessToken(authorizationBuilder,
+				generatedAccessToken, tokenContext);
+
+		OAuth2Authorization authorization = authorizationBuilder.build();
+
+		this.authorizationService.save(authorization);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Saved authorization");
+			// This log is kept separate for consistency with other providers
+			this.logger.trace("Authenticated token request");
+		}
+
+		return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken);
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2ClientCredentialsAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	/**
+	 * Sets the {@code Consumer} providing access to the
+	 * {@link OAuth2ClientCredentialsAuthenticationContext} and is responsible for
+	 * validating specific OAuth 2.0 Client Credentials Grant Request parameters
+	 * associated in the {@link OAuth2ClientCredentialsAuthenticationToken}. The default
+	 * authentication validator is {@link OAuth2ClientCredentialsAuthenticationValidator}.
+	 *
+	 * <p>
+	 * <b>NOTE:</b> The authentication validator MUST throw
+	 * {@link OAuth2AuthenticationException} if validation fails.
+	 * @param authenticationValidator the {@code Consumer} providing access to the
+	 * {@link OAuth2ClientCredentialsAuthenticationContext} and is responsible for
+	 * validating specific OAuth 2.0 Client Credentials Grant Request parameters
+	 * @since 1.3
+	 */
+	public void setAuthenticationValidator(
+			Consumer<OAuth2ClientCredentialsAuthenticationContext> authenticationValidator) {
+		Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
+		this.authenticationValidator = authenticationValidator;
+	}
+
+}

+ 61 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationToken.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization.authentication;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+
+/**
+ * An {@link Authentication} implementation used for the OAuth 2.0 Client Credentials
+ * Grant.
+ *
+ * @author Alexey Nesterov
+ * @since 0.0.1
+ * @see OAuth2AuthorizationGrantAuthenticationToken
+ * @see OAuth2ClientCredentialsAuthenticationProvider
+ */
+public class OAuth2ClientCredentialsAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
+
+	private final Set<String> scopes;
+
+	/**
+	 * Constructs an {@code OAuth2ClientCredentialsAuthenticationToken} using the provided
+	 * parameters.
+	 * @param clientPrincipal the authenticated client principal
+	 * @param scopes the requested scope(s)
+	 * @param additionalParameters the additional parameters
+	 */
+	public OAuth2ClientCredentialsAuthenticationToken(Authentication clientPrincipal, @Nullable Set<String> scopes,
+			@Nullable Map<String, Object> additionalParameters) {
+		super(AuthorizationGrantType.CLIENT_CREDENTIALS, clientPrincipal, additionalParameters);
+		this.scopes = Collections.unmodifiableSet((scopes != null) ? new HashSet<>(scopes) : Collections.emptySet());
+	}
+
+	/**
+	 * Returns the requested scope(s).
+	 * @return the requested scope(s), or an empty {@code Set} if not available
+	 */
+	public Set<String> getScopes() {
+		return this.scopes;
+	}
+
+}

+ 83 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationValidator.java

@@ -0,0 +1,83 @@
+/*
+ * Copyright 2020-2024 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.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.log.LogMessage;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+
+/**
+ * A {@code Consumer} providing access to the
+ * {@link OAuth2ClientCredentialsAuthenticationContext} containing an
+ * {@link OAuth2ClientCredentialsAuthenticationToken} and is the default
+ * {@link OAuth2ClientCredentialsAuthenticationProvider#setAuthenticationValidator(Consumer)
+ * authentication validator} used for validating specific OAuth 2.0 Client Credentials
+ * Grant Request parameters.
+ *
+ * <p>
+ * The default implementation validates
+ * {@link OAuth2ClientCredentialsAuthenticationToken#getScopes()}. If validation fails, an
+ * {@link OAuth2AuthenticationException} is thrown.
+ *
+ * @author Adam Pilling
+ * @since 1.3
+ * @see OAuth2ClientCredentialsAuthenticationContext
+ * @see OAuth2ClientCredentialsAuthenticationToken
+ * @see OAuth2ClientCredentialsAuthenticationProvider#setAuthenticationValidator(Consumer)
+ */
+public final class OAuth2ClientCredentialsAuthenticationValidator
+		implements Consumer<OAuth2ClientCredentialsAuthenticationContext> {
+
+	private static final Log LOGGER = LogFactory.getLog(OAuth2ClientCredentialsAuthenticationValidator.class);
+
+	/**
+	 * The default validator for
+	 * {@link OAuth2ClientCredentialsAuthenticationToken#getScopes()}.
+	 */
+	public static final Consumer<OAuth2ClientCredentialsAuthenticationContext> DEFAULT_SCOPE_VALIDATOR = OAuth2ClientCredentialsAuthenticationValidator::validateScope;
+
+	private final Consumer<OAuth2ClientCredentialsAuthenticationContext> authenticationValidator = DEFAULT_SCOPE_VALIDATOR;
+
+	@Override
+	public void accept(OAuth2ClientCredentialsAuthenticationContext authenticationContext) {
+		this.authenticationValidator.accept(authenticationContext);
+	}
+
+	private static void validateScope(OAuth2ClientCredentialsAuthenticationContext authenticationContext) {
+		OAuth2ClientCredentialsAuthenticationToken clientCredentialsAuthentication = authenticationContext
+			.getAuthentication();
+		RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
+
+		Set<String> requestedScopes = clientCredentialsAuthentication.getScopes();
+		Set<String> allowedScopes = registeredClient.getScopes();
+		if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
+			if (LOGGER.isDebugEnabled()) {
+				LOGGER.debug(LogMessage.format(
+						"Invalid request: requested scope is not allowed" + " for registered client '%s'",
+						registeredClient.getId()));
+			}
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
+		}
+	}
+
+}

+ 270 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationConsentAuthenticationProvider.java

@@ -0,0 +1,270 @@
+/*
+ * Copyright 2020-2024 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.HashSet;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2DeviceCode;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.OAuth2UserCode;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link AuthenticationProvider} implementation for the Device Authorization Consent
+ * used in the OAuth 2.0 Device Authorization Grant.
+ *
+ * @author Steve Riesenberg
+ * @since 1.1
+ * @see OAuth2DeviceAuthorizationConsentAuthenticationToken
+ * @see OAuth2AuthorizationConsent
+ * @see OAuth2DeviceAuthorizationRequestAuthenticationProvider
+ * @see OAuth2DeviceVerificationAuthenticationProvider
+ * @see OAuth2DeviceCodeAuthenticationProvider
+ * @see RegisteredClientRepository
+ * @see OAuth2AuthorizationService
+ * @see OAuth2AuthorizationConsentService
+ */
+public final class OAuth2DeviceAuthorizationConsentAuthenticationProvider implements AuthenticationProvider {
+
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
+	static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final RegisteredClientRepository registeredClientRepository;
+
+	private final OAuth2AuthorizationService authorizationService;
+
+	private final OAuth2AuthorizationConsentService authorizationConsentService;
+
+	private Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer;
+
+	/**
+	 * Constructs an {@code OAuth2DeviceAuthorizationConsentAuthenticationProvider} using
+	 * the provided parameters.
+	 * @param registeredClientRepository the repository of registered clients
+	 * @param authorizationService the authorization service
+	 * @param authorizationConsentService the authorization consent service
+	 */
+	public OAuth2DeviceAuthorizationConsentAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
+			OAuth2AuthorizationService authorizationService,
+			OAuth2AuthorizationConsentService authorizationConsentService) {
+		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
+		this.registeredClientRepository = registeredClientRepository;
+		this.authorizationService = authorizationService;
+		this.authorizationConsentService = authorizationConsentService;
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2DeviceAuthorizationConsentAuthenticationToken deviceAuthorizationConsentAuthentication = (OAuth2DeviceAuthorizationConsentAuthenticationToken) authentication;
+
+		OAuth2Authorization authorization = this.authorizationService
+			.findByToken(deviceAuthorizationConsentAuthentication.getState(), STATE_TOKEN_TYPE);
+		if (authorization == null) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved authorization with device authorization consent state");
+		}
+
+		// The authorization must be associated to the current principal
+		Authentication principal = (Authentication) deviceAuthorizationConsentAuthentication.getPrincipal();
+		if (!isPrincipalAuthenticated(principal) || !principal.getName().equals(authorization.getPrincipalName())) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
+		}
+
+		RegisteredClient registeredClient = this.registeredClientRepository
+			.findByClientId(deviceAuthorizationConsentAuthentication.getClientId());
+		if (registeredClient == null || !registeredClient.getId().equals(authorization.getRegisteredClientId())) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved registered client");
+		}
+
+		Set<String> requestedScopes = authorization.getAttribute(OAuth2ParameterNames.SCOPE);
+		Set<String> authorizedScopes = new HashSet<>(deviceAuthorizationConsentAuthentication.getScopes());
+		if (!requestedScopes.containsAll(authorizedScopes)) {
+			throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Validated device authorization consent request parameters");
+		}
+
+		OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService
+			.findById(authorization.getRegisteredClientId(), principal.getName());
+		Set<String> currentAuthorizedScopes = (currentAuthorizationConsent != null)
+				? currentAuthorizationConsent.getScopes() : Collections.emptySet();
+
+		if (!currentAuthorizedScopes.isEmpty()) {
+			for (String requestedScope : requestedScopes) {
+				if (currentAuthorizedScopes.contains(requestedScope)) {
+					authorizedScopes.add(requestedScope);
+				}
+			}
+		}
+
+		OAuth2AuthorizationConsent.Builder authorizationConsentBuilder;
+		if (currentAuthorizationConsent != null) {
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Retrieved existing authorization consent");
+			}
+			authorizationConsentBuilder = OAuth2AuthorizationConsent.from(currentAuthorizationConsent);
+		}
+		else {
+			authorizationConsentBuilder = OAuth2AuthorizationConsent.withId(authorization.getRegisteredClientId(),
+					principal.getName());
+		}
+		authorizedScopes.forEach(authorizationConsentBuilder::scope);
+
+		if (this.authorizationConsentCustomizer != null) {
+			// @formatter:off
+			OAuth2AuthorizationConsentAuthenticationContext authorizationConsentAuthenticationContext =
+					OAuth2AuthorizationConsentAuthenticationContext.with(deviceAuthorizationConsentAuthentication)
+							.authorizationConsent(authorizationConsentBuilder)
+							.registeredClient(registeredClient)
+							.authorization(authorization)
+							.build();
+			// @formatter:on
+			this.authorizationConsentCustomizer.accept(authorizationConsentAuthenticationContext);
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Customized authorization consent");
+			}
+		}
+
+		Set<GrantedAuthority> authorities = new HashSet<>();
+		authorizationConsentBuilder.authorities(authorities::addAll);
+
+		OAuth2Authorization.Token<OAuth2DeviceCode> deviceCodeToken = authorization.getToken(OAuth2DeviceCode.class);
+		OAuth2Authorization.Token<OAuth2UserCode> userCodeToken = authorization.getToken(OAuth2UserCode.class);
+
+		if (authorities.isEmpty()) {
+			// Authorization consent denied (or revoked)
+			if (currentAuthorizationConsent != null) {
+				this.authorizationConsentService.remove(currentAuthorizationConsent);
+				if (this.logger.isTraceEnabled()) {
+					this.logger.trace("Revoked authorization consent");
+				}
+			}
+			authorization = OAuth2Authorization.from(authorization)
+				.invalidate(deviceCodeToken.getToken())
+				.invalidate(userCodeToken.getToken())
+				.attributes((attrs) -> attrs.remove(OAuth2ParameterNames.STATE))
+				.build();
+			this.authorizationService.save(authorization);
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Invalidated device code and user code because authorization consent was denied");
+			}
+			throwError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID);
+		}
+
+		OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();
+		if (!authorizationConsent.equals(currentAuthorizationConsent)) {
+			this.authorizationConsentService.save(authorizationConsent);
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Saved authorization consent");
+			}
+		}
+
+		authorization = OAuth2Authorization.from(authorization)
+			.authorizedScopes(authorizedScopes)
+			.invalidate(userCodeToken.getToken())
+			.attributes((attrs) -> attrs.remove(OAuth2ParameterNames.STATE))
+			.attributes((attrs) -> attrs.remove(OAuth2ParameterNames.SCOPE))
+			.build();
+		this.authorizationService.save(authorization);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Saved authorization with authorized scopes");
+			// This log is kept separate for consistency with other providers
+			this.logger.trace("Authenticated device authorization consent request");
+		}
+
+		return new OAuth2DeviceVerificationAuthenticationToken(principal,
+				deviceAuthorizationConsentAuthentication.getUserCode(), registeredClient.getClientId());
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2DeviceAuthorizationConsentAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	/**
+	 * Sets the {@code Consumer} providing access to the
+	 * {@link OAuth2AuthorizationConsentAuthenticationContext} containing an
+	 * {@link OAuth2AuthorizationConsent.Builder} and additional context information.
+	 *
+	 * <p>
+	 * The following context attributes are available:
+	 * <ul>
+	 * <li>The {@link OAuth2AuthorizationConsent.Builder} used to build the authorization
+	 * consent prior to
+	 * {@link OAuth2AuthorizationConsentService#save(OAuth2AuthorizationConsent)}.</li>
+	 * <li>The {@link Authentication} of type
+	 * {@link OAuth2DeviceAuthorizationConsentAuthenticationToken}.</li>
+	 * <li>The {@link RegisteredClient} associated with the device authorization
+	 * request.</li>
+	 * <li>The {@link OAuth2Authorization} associated with the state token presented in
+	 * the device authorization consent request.</li>
+	 * </ul>
+	 * @param authorizationConsentCustomizer the {@code Consumer} providing access to the
+	 * {@link OAuth2AuthorizationConsentAuthenticationContext} containing an
+	 * {@link OAuth2AuthorizationConsent.Builder}
+	 */
+	public void setAuthorizationConsentCustomizer(
+			Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer) {
+		Assert.notNull(authorizationConsentCustomizer, "authorizationConsentCustomizer cannot be null");
+		this.authorizationConsentCustomizer = authorizationConsentCustomizer;
+	}
+
+	private static boolean isPrincipalAuthenticated(Authentication principal) {
+		return principal != null && !AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass())
+				&& principal.isAuthenticated();
+	}
+
+	private static void throwError(String errorCode, String parameterName) {
+		OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, ERROR_URI);
+		throw new OAuth2AuthenticationException(error);
+	}
+
+}

+ 106 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationConsentAuthenticationToken.java

@@ -0,0 +1,106 @@
+/*
+ * Copyright 2020-2025 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.io.Serial;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} implementation for the Device Authorization Consent used in
+ * the OAuth 2.0 Device Authorization Grant.
+ *
+ * @author Steve Riesenberg
+ * @since 1.1
+ * @see AbstractAuthenticationToken
+ * @see OAuth2DeviceAuthorizationConsentAuthenticationProvider
+ */
+public class OAuth2DeviceAuthorizationConsentAuthenticationToken extends OAuth2AuthorizationConsentAuthenticationToken {
+
+	@Serial
+	private static final long serialVersionUID = 3789252233721827596L;
+
+	private final String userCode;
+
+	private final Set<String> requestedScopes;
+
+	/**
+	 * Constructs an {@code OAuth2DeviceAuthorizationConsentAuthenticationToken} using the
+	 * provided parameters.
+	 * @param authorizationUri the authorization URI
+	 * @param clientId the client identifier
+	 * @param principal the {@code Principal} (Resource Owner)
+	 * @param userCode the user code associated with the device authorization response
+	 * @param state the state
+	 * @param authorizedScopes the authorized scope(s)
+	 * @param additionalParameters the additional parameters
+	 */
+	public OAuth2DeviceAuthorizationConsentAuthenticationToken(String authorizationUri, String clientId,
+			Authentication principal, String userCode, String state, @Nullable Set<String> authorizedScopes,
+			@Nullable Map<String, Object> additionalParameters) {
+		super(authorizationUri, clientId, principal, state, authorizedScopes, additionalParameters);
+		Assert.hasText(userCode, "userCode cannot be empty");
+		this.userCode = userCode;
+		this.requestedScopes = null;
+		setAuthenticated(false);
+	}
+
+	/**
+	 * Constructs an {@code OAuth2DeviceAuthorizationConsentAuthenticationToken} using the
+	 * provided parameters.
+	 * @param authorizationUri the authorization URI
+	 * @param clientId the client identifier
+	 * @param principal the {@code Principal} (Resource Owner)
+	 * @param userCode the user code associated with the device authorization response
+	 * @param state the state
+	 * @param requestedScopes the requested scope(s)
+	 * @param authorizedScopes the authorized scope(s)
+	 */
+	public OAuth2DeviceAuthorizationConsentAuthenticationToken(String authorizationUri, String clientId,
+			Authentication principal, String userCode, String state, @Nullable Set<String> requestedScopes,
+			@Nullable Set<String> authorizedScopes) {
+		super(authorizationUri, clientId, principal, state, authorizedScopes, null);
+		Assert.hasText(userCode, "userCode cannot be empty");
+		this.userCode = userCode;
+		this.requestedScopes = Collections
+			.unmodifiableSet((requestedScopes != null) ? new HashSet<>(requestedScopes) : Collections.emptySet());
+		setAuthenticated(true);
+	}
+
+	/**
+	 * Returns the user code.
+	 * @return the user code
+	 */
+	public String getUserCode() {
+		return this.userCode;
+	}
+
+	/**
+	 * Returns the requested scopes.
+	 * @return the requested scopes
+	 */
+	public Set<String> getRequestedScopes() {
+		return this.requestedScopes;
+	}
+
+}

+ 281 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationRequestAuthenticationProvider.java

@@ -0,0 +1,281 @@
+/*
+ * Copyright 2020-2024 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.time.Instant;
+import java.util.Base64;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.log.LogMessage;
+import org.springframework.lang.Nullable;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
+import org.springframework.security.crypto.keygen.BytesKeyGenerator;
+import org.springframework.security.crypto.keygen.KeyGenerators;
+import org.springframework.security.crypto.keygen.StringKeyGenerator;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2DeviceCode;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.OAuth2UserCode;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * An {@link AuthenticationProvider} implementation for the Device Authorization Request
+ * used in the OAuth 2.0 Device Authorization Grant.
+ *
+ * @author Steve Riesenberg
+ * @since 1.1
+ * @see OAuth2DeviceAuthorizationRequestAuthenticationToken
+ * @see OAuth2DeviceVerificationAuthenticationProvider
+ * @see OAuth2DeviceAuthorizationConsentAuthenticationProvider
+ * @see OAuth2DeviceCodeAuthenticationProvider
+ * @see OAuth2AuthorizationService
+ * @see OAuth2TokenGenerator
+ * @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8628">OAuth 2.0
+ * Device Authorization Grant</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc8628#section-3.1">Section 3.1 Device
+ * Authorization Request</a>
+ */
+public final class OAuth2DeviceAuthorizationRequestAuthenticationProvider implements AuthenticationProvider {
+
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
+	static final OAuth2TokenType DEVICE_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.DEVICE_CODE);
+	static final OAuth2TokenType USER_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.USER_CODE);
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final OAuth2AuthorizationService authorizationService;
+
+	private OAuth2TokenGenerator<OAuth2DeviceCode> deviceCodeGenerator = new OAuth2DeviceCodeGenerator();
+
+	private OAuth2TokenGenerator<OAuth2UserCode> userCodeGenerator = new OAuth2UserCodeGenerator();
+
+	/**
+	 * Constructs an {@code OAuth2DeviceAuthorizationRequestAuthenticationProvider} using
+	 * the provided parameters.
+	 * @param authorizationService the authorization service
+	 */
+	public OAuth2DeviceAuthorizationRequestAuthenticationProvider(OAuth2AuthorizationService authorizationService) {
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		this.authorizationService = authorizationService;
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2DeviceAuthorizationRequestAuthenticationToken deviceAuthorizationRequestAuthentication = (OAuth2DeviceAuthorizationRequestAuthenticationToken) authentication;
+
+		OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils
+			.getAuthenticatedClientElseThrowInvalidClient(deviceAuthorizationRequestAuthentication);
+		RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved registered client");
+		}
+
+		if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.DEVICE_CODE)) {
+			if (this.logger.isDebugEnabled()) {
+				this.logger.debug(LogMessage.format(
+						"Invalid request: requested grant_type is not allowed" + " for registered client '%s'",
+						registeredClient.getId()));
+			}
+			throwError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID);
+		}
+
+		Set<String> requestedScopes = deviceAuthorizationRequestAuthentication.getScopes();
+		if (!CollectionUtils.isEmpty(requestedScopes)) {
+			for (String requestedScope : requestedScopes) {
+				if (!registeredClient.getScopes().contains(requestedScope)) {
+					throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE);
+				}
+			}
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Validated device authorization request parameters");
+		}
+
+		// @formatter:off
+		DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
+				.registeredClient(registeredClient)
+				.principal(clientPrincipal)
+				.authorizationServerContext(AuthorizationServerContextHolder.getContext())
+				.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
+				.authorizationGrant(deviceAuthorizationRequestAuthentication);
+		// @formatter:on
+
+		// Generate a high-entropy string to use as the device code
+		OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(DEVICE_CODE_TOKEN_TYPE).build();
+		OAuth2DeviceCode deviceCode = this.deviceCodeGenerator.generate(tokenContext);
+		if (deviceCode == null) {
+			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
+					"The token generator failed to generate the device code.", ERROR_URI);
+			throw new OAuth2AuthenticationException(error);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Generated device code");
+		}
+
+		// Generate a low-entropy string to use as the user code
+		tokenContext = tokenContextBuilder.tokenType(USER_CODE_TOKEN_TYPE).build();
+		OAuth2UserCode userCode = this.userCodeGenerator.generate(tokenContext);
+		if (userCode == null) {
+			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
+					"The token generator failed to generate the user code.", ERROR_URI);
+			throw new OAuth2AuthenticationException(error);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Generated user code");
+		}
+
+		// @formatter:off
+		OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
+				.principalName(clientPrincipal.getName())
+				.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
+				.token(deviceCode)
+				.token(userCode)
+				.attribute(OAuth2ParameterNames.SCOPE, new HashSet<>(requestedScopes))
+				.build();
+		// @formatter:on
+		this.authorizationService.save(authorization);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Saved authorization");
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Authenticated device authorization request");
+		}
+
+		return new OAuth2DeviceAuthorizationRequestAuthenticationToken(clientPrincipal, requestedScopes, deviceCode,
+				userCode);
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2DeviceAuthorizationRequestAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	/**
+	 * Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2DeviceCode}.
+	 * @param deviceCodeGenerator the {@link OAuth2TokenGenerator} that generates the
+	 * {@link OAuth2DeviceCode}
+	 */
+	public void setDeviceCodeGenerator(OAuth2TokenGenerator<OAuth2DeviceCode> deviceCodeGenerator) {
+		Assert.notNull(deviceCodeGenerator, "deviceCodeGenerator cannot be null");
+		this.deviceCodeGenerator = deviceCodeGenerator;
+	}
+
+	/**
+	 * Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2UserCode}.
+	 * @param userCodeGenerator the {@link OAuth2TokenGenerator} that generates the
+	 * {@link OAuth2UserCode}
+	 */
+	public void setUserCodeGenerator(OAuth2TokenGenerator<OAuth2UserCode> userCodeGenerator) {
+		Assert.notNull(userCodeGenerator, "userCodeGenerator cannot be null");
+		this.userCodeGenerator = userCodeGenerator;
+	}
+
+	private static void throwError(String errorCode, String parameterName) {
+		OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, ERROR_URI);
+		throw new OAuth2AuthenticationException(error);
+	}
+
+	private static final class OAuth2DeviceCodeGenerator implements OAuth2TokenGenerator<OAuth2DeviceCode> {
+
+		private final StringKeyGenerator deviceCodeGenerator = new Base64StringKeyGenerator(
+				Base64.getUrlEncoder().withoutPadding(), 96);
+
+		@Nullable
+		@Override
+		public OAuth2DeviceCode generate(OAuth2TokenContext context) {
+			if (context.getTokenType() == null
+					|| !OAuth2ParameterNames.DEVICE_CODE.equals(context.getTokenType().getValue())) {
+				return null;
+			}
+			Instant issuedAt = Instant.now();
+			Instant expiresAt = issuedAt
+				.plus(context.getRegisteredClient().getTokenSettings().getDeviceCodeTimeToLive());
+			return new OAuth2DeviceCode(this.deviceCodeGenerator.generateKey(), issuedAt, expiresAt);
+		}
+
+	}
+
+	private static final class UserCodeStringKeyGenerator implements StringKeyGenerator {
+
+		// @formatter:off
+		private static final char[] VALID_CHARS = {
+				'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M',
+				'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Z'
+		};
+		// @formatter:on
+
+		private final BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom(8);
+
+		@Override
+		public String generateKey() {
+			byte[] bytes = this.keyGenerator.generateKey();
+			StringBuilder sb = new StringBuilder();
+			for (byte b : bytes) {
+				int offset = Math.abs(b % 20);
+				sb.append(VALID_CHARS[offset]);
+			}
+			sb.insert(4, '-');
+			return sb.toString();
+		}
+
+	}
+
+	private static final class OAuth2UserCodeGenerator implements OAuth2TokenGenerator<OAuth2UserCode> {
+
+		private final StringKeyGenerator userCodeGenerator = new UserCodeStringKeyGenerator();
+
+		@Nullable
+		@Override
+		public OAuth2UserCode generate(OAuth2TokenContext context) {
+			if (context.getTokenType() == null
+					|| !OAuth2ParameterNames.USER_CODE.equals(context.getTokenType().getValue())) {
+				return null;
+			}
+			Instant issuedAt = Instant.now();
+			Instant expiresAt = issuedAt
+				.plus(context.getRegisteredClient().getTokenSettings().getDeviceCodeTimeToLive());
+			return new OAuth2UserCode(this.userCodeGenerator.generateKey(), issuedAt, expiresAt);
+		}
+
+	}
+
+}

+ 154 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationRequestAuthenticationToken.java

@@ -0,0 +1,154 @@
+/*
+ * Copyright 2020-2025 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.io.Serial;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.OAuth2DeviceCode;
+import org.springframework.security.oauth2.core.OAuth2UserCode;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} implementation for the Device Authorization Request used in
+ * the OAuth 2.0 Device Authorization Grant.
+ *
+ * @author Steve Riesenberg
+ * @since 1.1
+ * @see AbstractAuthenticationToken
+ * @see OAuth2ClientAuthenticationToken
+ * @see OAuth2DeviceAuthorizationRequestAuthenticationProvider
+ */
+public class OAuth2DeviceAuthorizationRequestAuthenticationToken extends AbstractAuthenticationToken {
+
+	@Serial
+	private static final long serialVersionUID = -561059025431630645L;
+
+	private final Authentication clientPrincipal;
+
+	private final String authorizationUri;
+
+	private final Set<String> scopes;
+
+	private final OAuth2DeviceCode deviceCode;
+
+	private final OAuth2UserCode userCode;
+
+	private final Map<String, Object> additionalParameters;
+
+	/**
+	 * Constructs an {@code OAuth2DeviceAuthorizationRequestAuthenticationToken} using the
+	 * provided parameters.
+	 * @param clientPrincipal the authenticated client principal
+	 * @param authorizationUri the authorization {@code URI}
+	 * @param scopes the requested scope(s)
+	 * @param additionalParameters the additional parameters
+	 */
+	public OAuth2DeviceAuthorizationRequestAuthenticationToken(Authentication clientPrincipal, String authorizationUri,
+			@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
+		super(Collections.emptyList());
+		Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
+		Assert.hasText(authorizationUri, "authorizationUri cannot be empty");
+		this.clientPrincipal = clientPrincipal;
+		this.authorizationUri = authorizationUri;
+		this.scopes = Collections.unmodifiableSet((scopes != null) ? new HashSet<>(scopes) : Collections.emptySet());
+		this.additionalParameters = Collections.unmodifiableMap(
+				(additionalParameters != null) ? new HashMap<>(additionalParameters) : Collections.emptyMap());
+		this.deviceCode = null;
+		this.userCode = null;
+	}
+
+	/**
+	 * Constructs an {@code OAuth2DeviceAuthorizationRequestAuthenticationToken} using the
+	 * provided parameters.
+	 * @param clientPrincipal the authenticated client principal
+	 * @param scopes the requested scope(s)
+	 * @param deviceCode the {@link OAuth2DeviceCode}
+	 * @param userCode the {@link OAuth2UserCode}
+	 */
+	public OAuth2DeviceAuthorizationRequestAuthenticationToken(Authentication clientPrincipal,
+			@Nullable Set<String> scopes, OAuth2DeviceCode deviceCode, OAuth2UserCode userCode) {
+		super(Collections.emptyList());
+		Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
+		Assert.notNull(deviceCode, "deviceCode cannot be null");
+		Assert.notNull(userCode, "userCode cannot be null");
+		this.clientPrincipal = clientPrincipal;
+		this.scopes = Collections.unmodifiableSet((scopes != null) ? new HashSet<>(scopes) : Collections.emptySet());
+		this.deviceCode = deviceCode;
+		this.userCode = userCode;
+		this.authorizationUri = null;
+		this.additionalParameters = Collections.emptyMap();
+		setAuthenticated(true);
+	}
+
+	@Override
+	public Object getPrincipal() {
+		return this.clientPrincipal;
+	}
+
+	@Override
+	public Object getCredentials() {
+		return "";
+	}
+
+	/**
+	 * Returns the authorization {@code URI}.
+	 * @return the authorization {@code URI}
+	 */
+	public String getAuthorizationUri() {
+		return this.authorizationUri;
+	}
+
+	/**
+	 * Returns the requested scope(s).
+	 * @return the requested scope(s)
+	 */
+	public Set<String> getScopes() {
+		return this.scopes;
+	}
+
+	/**
+	 * Returns the device code.
+	 * @return the device code
+	 */
+	public OAuth2DeviceCode getDeviceCode() {
+		return this.deviceCode;
+	}
+
+	/**
+	 * Returns the user code.
+	 * @return the user code
+	 */
+	public OAuth2UserCode getUserCode() {
+		return this.userCode;
+	}
+
+	/**
+	 * Returns the additional parameters.
+	 * @return the additional parameters
+	 */
+	public Map<String, Object> getAdditionalParameters() {
+		return this.additionalParameters;
+	}
+
+}

+ 270 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProvider.java

@@ -0,0 +1,270 @@
+/*
+ * Copyright 2020-2025 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.security.Principal;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.log.LogMessage;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2DeviceCode;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.OAuth2RefreshToken;
+import org.springframework.security.oauth2.core.OAuth2Token;
+import org.springframework.security.oauth2.core.OAuth2UserCode;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link AuthenticationProvider} implementation for the Device Access Token Request
+ * used in the OAuth 2.0 Device Authorization Grant.
+ *
+ * @author Steve Riesenberg
+ * @since 1.1
+ * @see OAuth2DeviceCodeAuthenticationToken
+ * @see OAuth2AccessTokenAuthenticationToken
+ * @see OAuth2DeviceAuthorizationRequestAuthenticationProvider
+ * @see OAuth2DeviceVerificationAuthenticationProvider
+ * @see OAuth2DeviceAuthorizationConsentAuthenticationProvider
+ * @see OAuth2AuthorizationService
+ * @see OAuth2TokenGenerator
+ * @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8628">OAuth 2.0
+ * Device Authorization Grant</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc8628#section-3.4">Section 3.4 Device Access
+ * Token Request</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc8628#section-3.5">Section 3.5 Device Access
+ * Token Response</a>
+ */
+public final class OAuth2DeviceCodeAuthenticationProvider implements AuthenticationProvider {
+
+	private static final String DEFAULT_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
+
+	private static final String DEVICE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc8628#section-3.5";
+	static final OAuth2TokenType DEVICE_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.DEVICE_CODE);
+	static final String EXPIRED_TOKEN = "expired_token";
+	static final String AUTHORIZATION_PENDING = "authorization_pending";
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final OAuth2AuthorizationService authorizationService;
+
+	private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
+
+	/**
+	 * Constructs an {@code OAuth2DeviceCodeAuthenticationProvider} using the provided
+	 * parameters.
+	 * @param authorizationService the authorization service
+	 * @param tokenGenerator the token generator
+	 */
+	public OAuth2DeviceCodeAuthenticationProvider(OAuth2AuthorizationService authorizationService,
+			OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
+		this.authorizationService = authorizationService;
+		this.tokenGenerator = tokenGenerator;
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2DeviceCodeAuthenticationToken deviceCodeAuthentication = (OAuth2DeviceCodeAuthenticationToken) authentication;
+
+		OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils
+			.getAuthenticatedClientElseThrowInvalidClient(deviceCodeAuthentication);
+		RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved registered client");
+		}
+
+		OAuth2Authorization authorization = this.authorizationService
+			.findByToken(deviceCodeAuthentication.getDeviceCode(), DEVICE_CODE_TOKEN_TYPE);
+		if (authorization == null) {
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved authorization with device code");
+		}
+
+		OAuth2Authorization.Token<OAuth2UserCode> userCode = authorization.getToken(OAuth2UserCode.class);
+		OAuth2Authorization.Token<OAuth2DeviceCode> deviceCode = authorization.getToken(OAuth2DeviceCode.class);
+
+		if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {
+			if (!deviceCode.isInvalidated()) {
+				// Invalidate the device code given that a different client is attempting
+				// to use it
+				authorization = OAuth2Authorization.from(authorization).invalidate(deviceCode.getToken()).build();
+				this.authorizationService.save(authorization);
+				if (this.logger.isWarnEnabled()) {
+					this.logger.warn(LogMessage.format("Invalidated device code used by registered client '%s'",
+							authorization.getRegisteredClientId()));
+				}
+			}
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		// In https://www.rfc-editor.org/rfc/rfc8628.html#section-3.5,
+		// the following error codes are defined:
+
+		// expired_token
+		// The "device_code" has expired, and the device authorization
+		// session has concluded. The client MAY commence a new device
+		// authorization request but SHOULD wait for user interaction before
+		// restarting to avoid unnecessary polling.
+		if (deviceCode.isExpired()) {
+			if (!deviceCode.isInvalidated()) {
+				// Invalidate the device code
+				authorization = OAuth2Authorization.from(authorization).invalidate(deviceCode.getToken()).build();
+				this.authorizationService.save(authorization);
+				if (this.logger.isWarnEnabled()) {
+					this.logger.warn(LogMessage.format("Invalidated device code used by registered client '%s'",
+							authorization.getRegisteredClientId()));
+				}
+			}
+			OAuth2Error error = new OAuth2Error(EXPIRED_TOKEN, null, DEVICE_ERROR_URI);
+			throw new OAuth2AuthenticationException(error);
+		}
+
+		// authorization_pending
+		// The authorization request is still pending as the end user hasn't
+		// yet completed the user-interaction steps (Section 3.3). The
+		// client SHOULD repeat the access token request to the token
+		// endpoint (a process known as polling). Before each new request,
+		// the client MUST wait at least the number of seconds specified by
+		// the "interval" parameter of the device authorization response (see
+		// Section 3.2), or 5 seconds if none was provided, and respect any
+		// increase in the polling interval required by the "slow_down"
+		// error.
+		if (!userCode.isInvalidated()) {
+			OAuth2Error error = new OAuth2Error(AUTHORIZATION_PENDING, null, DEVICE_ERROR_URI);
+			throw new OAuth2AuthenticationException(error);
+		}
+
+		// slow_down
+		// A variant of "authorization_pending", the authorization request is
+		// still pending and polling should continue, but the interval MUST
+		// be increased by 5 seconds for this and all subsequent requests.
+		// NOTE: This error is not handled in the framework.
+
+		// access_denied
+		// The authorization request was denied.
+		if (deviceCode.isInvalidated()) {
+			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.ACCESS_DENIED, null, DEVICE_ERROR_URI);
+			throw new OAuth2AuthenticationException(error);
+		}
+
+		// Verify the DPoP Proof (if available)
+		Jwt dPoPProof = DPoPProofVerifier.verifyIfAvailable(deviceCodeAuthentication);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Validated device token request parameters");
+		}
+
+		// @formatter:off
+		DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
+				.registeredClient(registeredClient)
+				.principal(authorization.getAttribute(Principal.class.getName()))
+				.authorizationServerContext(AuthorizationServerContextHolder.getContext())
+				.authorization(authorization)
+				.authorizedScopes(authorization.getAuthorizedScopes())
+				.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
+				.authorizationGrant(deviceCodeAuthentication);
+		// @formatter:on
+		if (dPoPProof != null) {
+			tokenContextBuilder.put(OAuth2TokenContext.DPOP_PROOF_KEY, dPoPProof);
+		}
+
+		// @formatter:off
+		OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization)
+				// Invalidate the device code as it can only be used (successfully) once
+				.invalidate(deviceCode.getToken());
+		// @formatter:on
+
+		// ----- Access token -----
+		OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
+		OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
+		if (generatedAccessToken == null) {
+			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
+					"The token generator failed to generate the access token.", DEFAULT_ERROR_URI);
+			throw new OAuth2AuthenticationException(error);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Generated access token");
+		}
+
+		OAuth2AccessToken accessToken = OAuth2AuthenticationProviderUtils.accessToken(authorizationBuilder,
+				generatedAccessToken, tokenContext);
+
+		// ----- Refresh token -----
+		OAuth2RefreshToken refreshToken = null;
+		if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) {
+			tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
+			OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
+			if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
+				OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
+						"The token generator failed to generate the refresh token.", DEFAULT_ERROR_URI);
+				throw new OAuth2AuthenticationException(error);
+			}
+
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Generated refresh token");
+			}
+
+			refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
+			authorizationBuilder.refreshToken(refreshToken);
+		}
+
+		authorization = authorizationBuilder.build();
+
+		this.authorizationService.save(authorization);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Saved authorization");
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Authenticated device token request");
+		}
+
+		return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken);
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2DeviceCodeAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+}

+ 60 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationToken.java

@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020-2023 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.Map;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} implementation for the Device Access Token Request used in
+ * the OAuth 2.0 Device Authorization Grant.
+ *
+ * @author Steve Riesenberg
+ * @since 1.1
+ * @see OAuth2AuthorizationGrantAuthenticationToken
+ * @see OAuth2DeviceCodeAuthenticationProvider
+ */
+public class OAuth2DeviceCodeAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
+
+	private final String deviceCode;
+
+	/**
+	 * Constructs an {@code OAuth2DeviceCodeAuthenticationToken} using the provided
+	 * parameters.
+	 * @param deviceCode the device code
+	 * @param clientPrincipal the authenticated client principal
+	 * @param additionalParameters the additional parameters
+	 */
+	public OAuth2DeviceCodeAuthenticationToken(String deviceCode, Authentication clientPrincipal,
+			@Nullable Map<String, Object> additionalParameters) {
+		super(AuthorizationGrantType.DEVICE_CODE, clientPrincipal, additionalParameters);
+		Assert.hasText(deviceCode, "deviceCode cannot be empty");
+		this.deviceCode = deviceCode;
+	}
+
+	/**
+	 * Returns the device code.
+	 * @return the device code
+	 */
+	public String getDeviceCode() {
+		return this.deviceCode;
+	}
+
+}

+ 219 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationProvider.java

@@ -0,0 +1,219 @@
+/*
+ * Copyright 2020-2025 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.security.Principal;
+import java.util.Base64;
+import java.util.Set;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.log.LogMessage;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
+import org.springframework.security.crypto.keygen.StringKeyGenerator;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.OAuth2UserCode;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link AuthenticationProvider} implementation for the Device Verification Request
+ * (submission of the user code) used in the OAuth 2.0 Device Authorization Grant.
+ *
+ * @author Steve Riesenberg
+ * @since 1.1
+ * @see OAuth2DeviceVerificationAuthenticationToken
+ * @see OAuth2DeviceAuthorizationRequestAuthenticationProvider
+ * @see OAuth2DeviceAuthorizationConsentAuthenticationProvider
+ * @see OAuth2DeviceCodeAuthenticationProvider
+ * @see RegisteredClientRepository
+ * @see OAuth2AuthorizationService
+ * @see OAuth2AuthorizationConsentService
+ * @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8628">OAuth 2.0
+ * Device Authorization Grant</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc8628#section-3.3">Section 3.3 User
+ * Interaction</a>
+ */
+public final class OAuth2DeviceVerificationAuthenticationProvider implements AuthenticationProvider {
+
+	static final OAuth2TokenType USER_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.USER_CODE);
+
+	private static final StringKeyGenerator DEFAULT_STATE_GENERATOR = new Base64StringKeyGenerator(
+			Base64.getUrlEncoder());
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final RegisteredClientRepository registeredClientRepository;
+
+	private final OAuth2AuthorizationService authorizationService;
+
+	private final OAuth2AuthorizationConsentService authorizationConsentService;
+
+	/**
+	 * Constructs an {@code OAuth2DeviceVerificationAuthenticationProvider} using the
+	 * provided parameters.
+	 * @param registeredClientRepository the repository of registered clients
+	 * @param authorizationService the authorization service
+	 * @param authorizationConsentService the authorization consent service
+	 */
+	public OAuth2DeviceVerificationAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
+			OAuth2AuthorizationService authorizationService,
+			OAuth2AuthorizationConsentService authorizationConsentService) {
+		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
+		this.registeredClientRepository = registeredClientRepository;
+		this.authorizationService = authorizationService;
+		this.authorizationConsentService = authorizationConsentService;
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2DeviceVerificationAuthenticationToken deviceVerificationAuthentication = (OAuth2DeviceVerificationAuthenticationToken) authentication;
+
+		OAuth2Authorization authorization = this.authorizationService
+			.findByToken(deviceVerificationAuthentication.getUserCode(), USER_CODE_TOKEN_TYPE);
+		if (authorization == null) {
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved authorization with user code");
+		}
+
+		OAuth2Authorization.Token<OAuth2UserCode> userCode = authorization.getToken(OAuth2UserCode.class);
+		if (!userCode.isActive()) {
+			if (!userCode.isInvalidated()) {
+				authorization = OAuth2Authorization.from(authorization).invalidate(userCode.getToken()).build();
+				this.authorizationService.save(authorization);
+				if (this.logger.isWarnEnabled()) {
+					this.logger.warn(LogMessage.format("Invalidated user code used by registered client '%s'",
+							authorization.getRegisteredClientId()));
+				}
+			}
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		Authentication principal = (Authentication) deviceVerificationAuthentication.getPrincipal();
+		if (!isPrincipalAuthenticated(principal)) {
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Did not authenticate device verification request since principal not authenticated");
+			}
+			// Return the device verification request as-is where isAuthenticated() is
+			// false
+			return deviceVerificationAuthentication;
+		}
+
+		RegisteredClient registeredClient = this.registeredClientRepository
+			.findById(authorization.getRegisteredClientId());
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved registered client");
+		}
+
+		Set<String> requestedScopes = authorization.getAttribute(OAuth2ParameterNames.SCOPE);
+
+		OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService
+			.findById(registeredClient.getId(), principal.getName());
+
+		if (requiresAuthorizationConsent(requestedScopes, currentAuthorizationConsent)) {
+			String state = DEFAULT_STATE_GENERATOR.generateKey();
+			authorization = OAuth2Authorization.from(authorization)
+				.principalName(principal.getName())
+				.attribute(Principal.class.getName(), principal)
+				.attribute(OAuth2ParameterNames.STATE, state)
+				.build();
+
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Generated device authorization consent state");
+			}
+
+			this.authorizationService.save(authorization);
+
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Saved authorization");
+			}
+
+			Set<String> currentAuthorizedScopes = (currentAuthorizationConsent != null)
+					? currentAuthorizationConsent.getScopes() : null;
+
+			AuthorizationServerSettings authorizationServerSettings = AuthorizationServerContextHolder.getContext()
+				.getAuthorizationServerSettings();
+			String deviceVerificationUri = authorizationServerSettings.getDeviceVerificationEndpoint();
+
+			return new OAuth2DeviceAuthorizationConsentAuthenticationToken(deviceVerificationUri,
+					registeredClient.getClientId(), principal, deviceVerificationAuthentication.getUserCode(), state,
+					requestedScopes, currentAuthorizedScopes);
+		}
+
+		// @formatter:off
+		authorization = OAuth2Authorization.from(authorization)
+				.principalName(principal.getName())
+				.authorizedScopes(requestedScopes)
+				.invalidate(userCode.getToken())
+				.attribute(Principal.class.getName(), principal)
+				.attributes((attributes) -> attributes.remove(OAuth2ParameterNames.SCOPE))
+				.build();
+		// @formatter:on
+		this.authorizationService.save(authorization);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Saved authorization with authorized scopes");
+			// This log is kept separate for consistency with other providers
+			this.logger.trace("Authenticated device verification request");
+		}
+
+		return new OAuth2DeviceVerificationAuthenticationToken(principal,
+				deviceVerificationAuthentication.getUserCode(), registeredClient.getClientId());
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2DeviceVerificationAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	private static boolean requiresAuthorizationConsent(Set<String> requestedScopes,
+			OAuth2AuthorizationConsent authorizationConsent) {
+
+		if (authorizationConsent != null && authorizationConsent.getScopes().containsAll(requestedScopes)) {
+			return false;
+		}
+
+		return true;
+	}
+
+	private static boolean isPrincipalAuthenticated(Authentication principal) {
+		return principal != null && !AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass())
+				&& principal.isAuthenticated();
+	}
+
+}

+ 122 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationToken.java

@@ -0,0 +1,122 @@
+/*
+ * Copyright 2020-2025 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.io.Serial;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} implementation for the Device Verification Request
+ * (submission of the user code) used in the OAuth 2.0 Device Authorization Grant.
+ *
+ * @author Steve Riesenberg
+ * @since 1.1
+ * @see AbstractAuthenticationToken
+ * @see OAuth2DeviceVerificationAuthenticationProvider
+ */
+public class OAuth2DeviceVerificationAuthenticationToken extends AbstractAuthenticationToken {
+
+	@Serial
+	private static final long serialVersionUID = -2164261941629756913L;
+
+	private final Authentication principal;
+
+	private final String userCode;
+
+	private final Map<String, Object> additionalParameters;
+
+	private final String clientId;
+
+	/**
+	 * Constructs an {@code OAuth2DeviceVerificationAuthenticationToken} using the
+	 * provided parameters.
+	 * @param principal the {@code Principal} (Resource Owner)
+	 * @param userCode the user code associated with the device authorization response
+	 * @param additionalParameters the additional parameters
+	 */
+	public OAuth2DeviceVerificationAuthenticationToken(Authentication principal, String userCode,
+			@Nullable Map<String, Object> additionalParameters) {
+		super(Collections.emptyList());
+		Assert.notNull(principal, "principal cannot be null");
+		Assert.hasText(userCode, "userCode cannot be empty");
+		this.principal = principal;
+		this.userCode = userCode;
+		this.additionalParameters = Collections.unmodifiableMap(
+				(additionalParameters != null) ? new HashMap<>(additionalParameters) : Collections.emptyMap());
+		this.clientId = null;
+	}
+
+	/**
+	 * Constructs an {@code OAuth2DeviceVerificationAuthenticationToken} using the
+	 * provided parameters.
+	 * @param principal the {@code Principal} (Resource Owner)
+	 * @param userCode the user code associated with the device authorization response
+	 * @param clientId the client identifier
+	 */
+	public OAuth2DeviceVerificationAuthenticationToken(Authentication principal, String userCode, String clientId) {
+		super(Collections.emptyList());
+		Assert.notNull(principal, "principal cannot be null");
+		Assert.hasText(userCode, "userCode cannot be empty");
+		Assert.hasText(clientId, "clientId cannot be empty");
+		this.principal = principal;
+		this.userCode = userCode;
+		this.clientId = clientId;
+		this.additionalParameters = Collections.emptyMap();
+		setAuthenticated(true);
+	}
+
+	@Override
+	public Object getPrincipal() {
+		return this.principal;
+	}
+
+	@Override
+	public Object getCredentials() {
+		return "";
+	}
+
+	/**
+	 * Returns the user code.
+	 * @return the user code
+	 */
+	public String getUserCode() {
+		return this.userCode;
+	}
+
+	/**
+	 * Returns the additional parameters.
+	 * @return the additional parameters, or an empty {@code Map} if not available
+	 */
+	public Map<String, Object> getAdditionalParameters() {
+		return this.additionalParameters;
+	}
+
+	/**
+	 * Returns the client identifier.
+	 * @return the client identifier
+	 */
+	public String getClientId() {
+		return this.clientId;
+	}
+
+}

+ 184 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestAuthenticationProvider.java

@@ -0,0 +1,184 @@
+/*
+ * Copyright 2020-2025 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.function.Consumer;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Pushed Authorization
+ * Request used in the Authorization Code Grant.
+ *
+ * @author Joe Grandja
+ * @since 1.5
+ * @see OAuth2PushedAuthorizationRequestAuthenticationToken
+ * @see OAuth2AuthorizationCodeRequestAuthenticationToken
+ * @see OAuth2AuthorizationCodeRequestAuthenticationValidator
+ * @see OAuth2AuthorizationService
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc9126#section-2.1">Section 2.1 Pushed
+ * Authorization Request</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc9126#section-2.2">Section 2.2 Pushed
+ * Authorization Response</a>
+ */
+public final class OAuth2PushedAuthorizationRequestAuthenticationProvider implements AuthenticationProvider {
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final OAuth2AuthorizationService authorizationService;
+
+	private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator = new OAuth2AuthorizationCodeRequestAuthenticationValidator();
+
+	/**
+	 * Constructs an {@code OAuth2PushedAuthorizationRequestAuthenticationProvider} using
+	 * the provided parameters.
+	 * @param authorizationService the authorization service
+	 */
+	public OAuth2PushedAuthorizationRequestAuthenticationProvider(OAuth2AuthorizationService authorizationService) {
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		this.authorizationService = authorizationService;
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationRequestAuthentication = (OAuth2PushedAuthorizationRequestAuthenticationToken) authentication;
+
+		OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils
+			.getAuthenticatedClientElseThrowInvalidClient(pushedAuthorizationRequestAuthentication);
+		RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved registered client");
+		}
+
+		OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext = OAuth2AuthorizationCodeRequestAuthenticationContext
+			.with(toAuthorizationCodeRequestAuthentication(pushedAuthorizationRequestAuthentication))
+			.registeredClient(registeredClient)
+			.build();
+
+		// grant_type
+		OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_AUTHORIZATION_GRANT_TYPE_VALIDATOR
+			.accept(authenticationContext);
+
+		// redirect_uri and scope
+		this.authenticationValidator.accept(authenticationContext);
+
+		// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
+		OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_CODE_CHALLENGE_VALIDATOR
+			.accept(authenticationContext);
+
+		// prompt (OPTIONAL for OpenID Connect 1.0 Authentication Request)
+		OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_PROMPT_VALIDATOR.accept(authenticationContext);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Validated pushed authorization request parameters");
+		}
+
+		// @formatter:off
+		OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
+			.authorizationUri(pushedAuthorizationRequestAuthentication.getAuthorizationUri())
+			.clientId(registeredClient.getClientId())
+			.redirectUri(pushedAuthorizationRequestAuthentication.getRedirectUri())
+			.scopes(pushedAuthorizationRequestAuthentication.getScopes())
+			.state(pushedAuthorizationRequestAuthentication.getState())
+			.additionalParameters(pushedAuthorizationRequestAuthentication.getAdditionalParameters())
+			.build();
+		// @formatter:on
+
+		OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = OAuth2PushedAuthorizationRequestUri
+			.create();
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Generated pushed authorization request uri");
+		}
+
+		// @formatter:off
+		OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
+				.principalName(clientPrincipal.getName())
+				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+				.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest)
+				.attribute(OAuth2ParameterNames.STATE, pushedAuthorizationRequestUri.getState())
+				.build();
+		// @formatter:on
+		this.authorizationService.save(authorization);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Saved authorization");
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Authenticated pushed authorization request");
+		}
+
+		return new OAuth2PushedAuthorizationRequestAuthenticationToken(authorizationRequest.getAuthorizationUri(),
+				authorizationRequest.getClientId(), clientPrincipal, pushedAuthorizationRequestUri.getRequestUri(),
+				pushedAuthorizationRequestUri.getExpiresAt(), authorizationRequest.getRedirectUri(),
+				authorizationRequest.getState(), authorizationRequest.getScopes());
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2PushedAuthorizationRequestAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	/**
+	 * Sets the {@code Consumer} providing access to the
+	 * {@link OAuth2AuthorizationCodeRequestAuthenticationContext} and is responsible for
+	 * validating specific OAuth 2.0 Pushed Authorization Request parameters associated in
+	 * the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}. The default
+	 * authentication validator is
+	 * {@link OAuth2AuthorizationCodeRequestAuthenticationValidator}.
+	 *
+	 * <p>
+	 * <b>NOTE:</b> The authentication validator MUST throw
+	 * {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails.
+	 * @param authenticationValidator the {@code Consumer} providing access to the
+	 * {@link OAuth2AuthorizationCodeRequestAuthenticationContext} and is responsible for
+	 * validating specific OAuth 2.0 Pushed Authorization Request parameters
+	 */
+	public void setAuthenticationValidator(
+			Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) {
+		Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
+		this.authenticationValidator = authenticationValidator;
+	}
+
+	private static OAuth2AuthorizationCodeRequestAuthenticationToken toAuthorizationCodeRequestAuthentication(
+			OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationCodeRequestAuthentication) {
+		return new OAuth2AuthorizationCodeRequestAuthenticationToken(
+				pushedAuthorizationCodeRequestAuthentication.getAuthorizationUri(),
+				pushedAuthorizationCodeRequestAuthentication.getClientId(),
+				(Authentication) pushedAuthorizationCodeRequestAuthentication.getPrincipal(),
+				pushedAuthorizationCodeRequestAuthentication.getRedirectUri(),
+				pushedAuthorizationCodeRequestAuthentication.getState(),
+				pushedAuthorizationCodeRequestAuthentication.getScopes(),
+				pushedAuthorizationCodeRequestAuthentication.getAdditionalParameters());
+	}
+
+}

+ 109 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestAuthenticationToken.java

@@ -0,0 +1,109 @@
+/*
+ * Copyright 2020-2025 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.io.Serial;
+import java.time.Instant;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Authentication;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} implementation for the OAuth 2.0 Pushed Authorization Request
+ * used in the Authorization Code Grant.
+ *
+ * @author Joe Grandja
+ * @since 1.5
+ * @see OAuth2PushedAuthorizationRequestAuthenticationProvider
+ */
+public class OAuth2PushedAuthorizationRequestAuthenticationToken
+		extends AbstractOAuth2AuthorizationCodeRequestAuthenticationToken {
+
+	@Serial
+	private static final long serialVersionUID = 7330534287786569644L;
+
+	private final String requestUri;
+
+	private final Instant requestUriExpiresAt;
+
+	/**
+	 * Constructs an {@code OAuth2PushedAuthorizationRequestAuthenticationToken} using the
+	 * provided parameters.
+	 * @param authorizationUri the authorization URI
+	 * @param clientId the client identifier
+	 * @param principal the authenticated client principal
+	 * @param redirectUri the redirect uri
+	 * @param state the state
+	 * @param scopes the requested scope(s)
+	 * @param additionalParameters the additional parameters
+	 */
+	public OAuth2PushedAuthorizationRequestAuthenticationToken(String authorizationUri, String clientId,
+			Authentication principal, @Nullable String redirectUri, @Nullable String state,
+			@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
+		super(authorizationUri, clientId, principal, redirectUri, state, scopes, additionalParameters);
+		this.requestUri = null;
+		this.requestUriExpiresAt = null;
+	}
+
+	/**
+	 * Constructs an {@code OAuth2PushedAuthorizationRequestAuthenticationToken} using the
+	 * provided parameters.
+	 * @param authorizationUri the authorization URI
+	 * @param clientId the client identifier
+	 * @param principal the authenticated client principal
+	 * @param requestUri the {@code request_uri} corresponding to the authorization
+	 * request posted
+	 * @param requestUriExpiresAt the expiration time on or after which the
+	 * {@code request_uri} MUST NOT be accepted
+	 * @param redirectUri the redirect uri
+	 * @param state the state
+	 * @param scopes the authorized scope(s)
+	 */
+	public OAuth2PushedAuthorizationRequestAuthenticationToken(String authorizationUri, String clientId,
+			Authentication principal, String requestUri, Instant requestUriExpiresAt, @Nullable String redirectUri,
+			@Nullable String state, @Nullable Set<String> scopes) {
+		super(authorizationUri, clientId, principal, redirectUri, state, scopes, null);
+		Assert.hasText(requestUri, "requestUri cannot be empty");
+		Assert.notNull(requestUriExpiresAt, "requestUriExpiresAt cannot be null");
+		this.requestUri = requestUri;
+		this.requestUriExpiresAt = requestUriExpiresAt;
+		setAuthenticated(true);
+	}
+
+	/**
+	 * Returns the {@code request_uri} corresponding to the authorization request posted.
+	 * @return the {@code request_uri} corresponding to the authorization request posted
+	 */
+	@Nullable
+	public String getRequestUri() {
+		return this.requestUri;
+	}
+
+	/**
+	 * Returns the expiration time on or after which the {@code request_uri} MUST NOT be
+	 * accepted.
+	 * @return the expiration time on or after which the {@code request_uri} MUST NOT be
+	 * accepted
+	 */
+	@Nullable
+	public Instant getRequestUriExpiresAt() {
+		return this.requestUriExpiresAt;
+	}
+
+}

+ 86 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestUri.java

@@ -0,0 +1,86 @@
+/*
+ * Copyright 2020-2025 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.time.Instant;
+import java.util.Base64;
+
+import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
+import org.springframework.security.crypto.keygen.StringKeyGenerator;
+
+/**
+ * A representation of a {@code request_uri} used in OAuth 2.0 Pushed Authorization
+ * Requests.
+ *
+ * @author Joe Grandja
+ * @since 1.5
+ */
+final class OAuth2PushedAuthorizationRequestUri {
+
+	private static final String REQUEST_URI_PREFIX = "urn:ietf:params:oauth:request_uri:";
+
+	private static final String REQUEST_URI_DELIMITER = "___";
+
+	private static final StringKeyGenerator DEFAULT_STATE_GENERATOR = new Base64StringKeyGenerator(
+			Base64.getUrlEncoder());
+
+	private String requestUri;
+
+	private String state;
+
+	private Instant expiresAt;
+
+	static OAuth2PushedAuthorizationRequestUri create() {
+		return create(Instant.now().plusSeconds(300));
+	}
+
+	static OAuth2PushedAuthorizationRequestUri create(Instant expiresAt) {
+		String state = DEFAULT_STATE_GENERATOR.generateKey();
+		OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = new OAuth2PushedAuthorizationRequestUri();
+		pushedAuthorizationRequestUri.requestUri = REQUEST_URI_PREFIX + state + REQUEST_URI_DELIMITER
+				+ expiresAt.toEpochMilli();
+		pushedAuthorizationRequestUri.state = state + REQUEST_URI_DELIMITER + expiresAt.toEpochMilli();
+		pushedAuthorizationRequestUri.expiresAt = expiresAt;
+		return pushedAuthorizationRequestUri;
+	}
+
+	static OAuth2PushedAuthorizationRequestUri parse(String requestUri) {
+		int stateStartIndex = REQUEST_URI_PREFIX.length();
+		int expiresAtStartIndex = requestUri.indexOf(REQUEST_URI_DELIMITER) + REQUEST_URI_DELIMITER.length();
+		OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = new OAuth2PushedAuthorizationRequestUri();
+		pushedAuthorizationRequestUri.requestUri = requestUri;
+		pushedAuthorizationRequestUri.state = requestUri.substring(stateStartIndex);
+		pushedAuthorizationRequestUri.expiresAt = Instant
+			.ofEpochMilli(Long.parseLong(requestUri.substring(expiresAtStartIndex)));
+		return pushedAuthorizationRequestUri;
+	}
+
+	String getRequestUri() {
+		return this.requestUri;
+	}
+
+	String getState() {
+		return this.state;
+	}
+
+	Instant getExpiresAt() {
+		return this.expiresAt;
+	}
+
+	private OAuth2PushedAuthorizationRequestUri() {
+	}
+
+}

+ 336 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java

@@ -0,0 +1,336 @@
+/*
+ * Copyright 2020-2025 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.security.Principal;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import com.nimbusds.jose.jwk.JWK;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.log.LogMessage;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClaimAccessor;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+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.OAuth2RefreshToken;
+import org.springframework.security.oauth2.core.OAuth2Token;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.OidcScopes;
+import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Refresh Token Grant.
+ *
+ * @author Alexey Nesterov
+ * @author Joe Grandja
+ * @author Anoop Garlapati
+ * @since 0.0.3
+ * @see OAuth2RefreshTokenAuthenticationToken
+ * @see OAuth2AccessTokenAuthenticationToken
+ * @see OAuth2AuthorizationService
+ * @see OAuth2TokenGenerator
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc6749#section-1.5">Section 1.5 Refresh Token
+ * Grant</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc6749#section-6">Section 6 Refreshing an
+ * Access Token</a>
+ */
+public final class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationProvider {
+
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
+
+	private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final OAuth2AuthorizationService authorizationService;
+
+	private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
+
+	/**
+	 * Constructs an {@code OAuth2RefreshTokenAuthenticationProvider} using the provided
+	 * parameters.
+	 * @param authorizationService the authorization service
+	 * @param tokenGenerator the token generator
+	 * @since 0.2.3
+	 */
+	public OAuth2RefreshTokenAuthenticationProvider(OAuth2AuthorizationService authorizationService,
+			OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
+		this.authorizationService = authorizationService;
+		this.tokenGenerator = tokenGenerator;
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2RefreshTokenAuthenticationToken refreshTokenAuthentication = (OAuth2RefreshTokenAuthenticationToken) authentication;
+
+		OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils
+			.getAuthenticatedClientElseThrowInvalidClient(refreshTokenAuthentication);
+		RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved registered client");
+		}
+
+		OAuth2Authorization authorization = this.authorizationService
+			.findByToken(refreshTokenAuthentication.getRefreshToken(), OAuth2TokenType.REFRESH_TOKEN);
+		if (authorization == null) {
+			if (this.logger.isDebugEnabled()) {
+				this.logger.debug("Invalid request: refresh_token is invalid");
+			}
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved authorization with refresh token");
+		}
+
+		if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) {
+			if (this.logger.isDebugEnabled()) {
+				this.logger.debug(LogMessage.format(
+						"Invalid request: requested grant_type is not allowed" + " for registered client '%s'",
+						registeredClient.getId()));
+			}
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
+		}
+
+		OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken = authorization.getRefreshToken();
+		if (!refreshToken.isActive()) {
+			// As per https://tools.ietf.org/html/rfc6749#section-5.2
+			// invalid_grant: The provided authorization grant (e.g., authorization code,
+			// resource owner credentials) or refresh token is invalid, expired, revoked
+			// [...].
+			if (this.logger.isDebugEnabled()) {
+				this.logger.debug(LogMessage.format(
+						"Invalid request: refresh_token is not active" + " for registered client '%s'",
+						registeredClient.getId()));
+			}
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		// As per https://tools.ietf.org/html/rfc6749#section-6
+		// The requested scope MUST NOT include any scope not originally granted by the
+		// resource owner,
+		// and if omitted is treated as equal to the scope originally granted by the
+		// resource owner.
+		Set<String> scopes = refreshTokenAuthentication.getScopes();
+		Set<String> authorizedScopes = authorization.getAuthorizedScopes();
+		if (!authorizedScopes.containsAll(scopes)) {
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
+		}
+
+		// Verify the DPoP Proof (if available)
+		Jwt dPoPProof = DPoPProofVerifier.verifyIfAvailable(refreshTokenAuthentication);
+
+		if (dPoPProof != null
+				&& clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
+			// For public clients, verify the DPoP Proof public key is same as (current)
+			// access token public key binding
+			Map<String, Object> accessTokenClaims = authorization.getAccessToken().getClaims();
+			verifyDPoPProofPublicKey(dPoPProof, () -> accessTokenClaims);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Validated token request parameters");
+		}
+
+		if (scopes.isEmpty()) {
+			scopes = authorizedScopes;
+		}
+
+		// @formatter:off
+		DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
+				.registeredClient(registeredClient)
+				.principal(authorization.getAttribute(Principal.class.getName()))
+				.authorizationServerContext(AuthorizationServerContextHolder.getContext())
+				.authorization(authorization)
+				.authorizedScopes(scopes)
+				.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
+				.authorizationGrant(refreshTokenAuthentication);
+		// @formatter:on
+		if (dPoPProof != null) {
+			tokenContextBuilder.put(OAuth2TokenContext.DPOP_PROOF_KEY, dPoPProof);
+		}
+
+		OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization);
+
+		// ----- Access token -----
+		OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
+		OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
+		if (generatedAccessToken == null) {
+			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
+					"The token generator failed to generate the access token.", ERROR_URI);
+			throw new OAuth2AuthenticationException(error);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Generated access token");
+		}
+
+		OAuth2AccessToken accessToken = OAuth2AuthenticationProviderUtils.accessToken(authorizationBuilder,
+				generatedAccessToken, tokenContext);
+
+		// ----- Refresh token -----
+		OAuth2RefreshToken currentRefreshToken = refreshToken.getToken();
+		if (!registeredClient.getTokenSettings().isReuseRefreshTokens()) {
+			// @formatter:off
+			tokenContext = tokenContextBuilder
+					.tokenType(OAuth2TokenType.REFRESH_TOKEN)
+					.authorization(authorizationBuilder.build())	// Refresh token generator/customizer may need access to the access token
+					.build();
+			// @formatter:on
+			OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
+			if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
+				OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
+						"The token generator failed to generate the refresh token.", ERROR_URI);
+				throw new OAuth2AuthenticationException(error);
+			}
+
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Generated refresh token");
+			}
+
+			currentRefreshToken = (OAuth2RefreshToken) generatedRefreshToken;
+			authorizationBuilder.refreshToken(currentRefreshToken);
+		}
+
+		// ----- ID token -----
+		OidcIdToken idToken;
+		if (authorizedScopes.contains(OidcScopes.OPENID)) {
+			// @formatter:off
+			tokenContext = tokenContextBuilder
+					.tokenType(ID_TOKEN_TOKEN_TYPE)
+					.authorization(authorizationBuilder.build())	// ID token customizer may need access to the access token and/or refresh token
+					.build();
+			// @formatter:on
+			OAuth2Token generatedIdToken = this.tokenGenerator.generate(tokenContext);
+			if (!(generatedIdToken instanceof Jwt)) {
+				OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
+						"The token generator failed to generate the ID token.", ERROR_URI);
+				throw new OAuth2AuthenticationException(error);
+			}
+
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Generated id token");
+			}
+
+			idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(),
+					generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims());
+			authorizationBuilder.token(idToken,
+					(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()));
+		}
+		else {
+			idToken = null;
+		}
+
+		authorization = authorizationBuilder.build();
+
+		this.authorizationService.save(authorization);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Saved authorization");
+		}
+
+		Map<String, Object> additionalParameters = Collections.emptyMap();
+		if (idToken != null) {
+			additionalParameters = new HashMap<>();
+			additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue());
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Authenticated token request");
+		}
+
+		return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken,
+				currentRefreshToken, additionalParameters);
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2RefreshTokenAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	private static void verifyDPoPProofPublicKey(Jwt dPoPProof, ClaimAccessor accessTokenClaims) {
+		JWK jwk = null;
+		@SuppressWarnings("unchecked")
+		Map<String, Object> jwkJson = (Map<String, Object>) dPoPProof.getHeaders().get("jwk");
+		try {
+			jwk = JWK.parse(jwkJson);
+		}
+		catch (Exception ignored) {
+		}
+		if (jwk == null) {
+			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF,
+					"jwk header is missing or invalid.", null);
+			throw new OAuth2AuthenticationException(error);
+		}
+
+		String jwkThumbprint;
+		try {
+			jwkThumbprint = jwk.computeThumbprint().toString();
+		}
+		catch (Exception ex) {
+			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF,
+					"Failed to compute SHA-256 Thumbprint for jwk.", null);
+			throw new OAuth2AuthenticationException(error);
+		}
+
+		String jwkThumbprintClaim = null;
+		Map<String, Object> confirmationMethodClaim = accessTokenClaims.getClaimAsMap("cnf");
+		if (!CollectionUtils.isEmpty(confirmationMethodClaim) && confirmationMethodClaim.containsKey("jkt")) {
+			jwkThumbprintClaim = (String) confirmationMethodClaim.get("jkt");
+		}
+		if (jwkThumbprintClaim == null) {
+			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF, "jkt claim is missing.", null);
+			throw new OAuth2AuthenticationException(error);
+		}
+
+		if (!jwkThumbprint.equals(jwkThumbprintClaim)) {
+			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF, "jwk header is invalid.", null);
+			throw new OAuth2AuthenticationException(error);
+		}
+	}
+
+}

+ 74 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationToken.java

@@ -0,0 +1,74 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization.authentication;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} implementation used for the OAuth 2.0 Refresh Token Grant.
+ *
+ * @author Alexey Nesterov
+ * @since 0.0.3
+ * @see OAuth2AuthorizationGrantAuthenticationToken
+ * @see OAuth2RefreshTokenAuthenticationProvider
+ */
+public class OAuth2RefreshTokenAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
+
+	private final String refreshToken;
+
+	private final Set<String> scopes;
+
+	/**
+	 * Constructs an {@code OAuth2RefreshTokenAuthenticationToken} using the provided
+	 * parameters.
+	 * @param refreshToken the refresh token
+	 * @param clientPrincipal the authenticated client principal
+	 * @param scopes the requested scope(s)
+	 * @param additionalParameters the additional parameters
+	 */
+	public OAuth2RefreshTokenAuthenticationToken(String refreshToken, Authentication clientPrincipal,
+			@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
+		super(AuthorizationGrantType.REFRESH_TOKEN, clientPrincipal, additionalParameters);
+		Assert.hasText(refreshToken, "refreshToken cannot be empty");
+		this.refreshToken = refreshToken;
+		this.scopes = Collections.unmodifiableSet((scopes != null) ? new HashSet<>(scopes) : Collections.emptySet());
+	}
+
+	/**
+	 * Returns the refresh token.
+	 * @return the refresh token
+	 */
+	public String getRefreshToken() {
+		return this.refreshToken;
+	}
+
+	/**
+	 * Returns the requested scope(s).
+	 * @return the requested scope(s), or an empty {@code Set} if not available
+	 */
+	public Set<String> getScopes() {
+		return this.scopes;
+	}
+
+}

+ 71 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeActor.java

@@ -0,0 +1,71 @@
+/*
+ * Copyright 2020-2024 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.Map;
+import java.util.Objects;
+
+import org.springframework.security.oauth2.core.ClaimAccessor;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimNames;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link ClaimAccessor} used for the OAuth 2.0 Token Exchange Grant to represent an
+ * actor in a {@link OAuth2TokenExchangeCompositeAuthenticationToken} (e.g. the
+ * "delegation" use case).
+ *
+ * @author Steve Riesenberg
+ * @since 1.3
+ * @see OAuth2TokenExchangeCompositeAuthenticationToken
+ */
+public final class OAuth2TokenExchangeActor implements ClaimAccessor {
+
+	private final Map<String, Object> claims;
+
+	public OAuth2TokenExchangeActor(Map<String, Object> claims) {
+		Assert.notNull(claims, "claims cannot be null");
+		this.claims = Collections.unmodifiableMap(claims);
+	}
+
+	@Override
+	public Map<String, Object> getClaims() {
+		return this.claims;
+	}
+
+	public String getIssuer() {
+		return getClaimAsString(OAuth2TokenClaimNames.ISS);
+	}
+
+	public String getSubject() {
+		return getClaimAsString(OAuth2TokenClaimNames.SUB);
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (!(obj instanceof OAuth2TokenExchangeActor other)) {
+			return false;
+		}
+		return Objects.equals(this.claims, other.claims);
+	}
+
+	@Override
+	public int hashCode() {
+		return Objects.hash(this.claims);
+	}
+
+}

+ 333 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeAuthenticationProvider.java

@@ -0,0 +1,333 @@
+/*
+ * Copyright 2020-2025 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.security.Principal;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+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.OAuth2Token;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
+import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimNames;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Token Exchange
+ * Grant.
+ *
+ * @author Steve Riesenberg
+ * @since 1.3
+ * @see OAuth2TokenExchangeAuthenticationToken
+ * @see OAuth2AccessTokenAuthenticationToken
+ * @see OAuth2AuthorizationService
+ * @see OAuth2TokenGenerator
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc8693#section-1">Section 1 Introduction</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc8693#section-2.1">Section 2.1 Request</a>
+ */
+public final class OAuth2TokenExchangeAuthenticationProvider implements AuthenticationProvider {
+
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
+
+	private static final String JWT_TOKEN_TYPE_VALUE = "urn:ietf:params:oauth:token-type:jwt";
+
+	private static final String ACCESS_TOKEN_TYPE_VALUE = "urn:ietf:params:oauth:token-type:access_token";
+
+	private static final String MAY_ACT = "may_act";
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final OAuth2AuthorizationService authorizationService;
+
+	private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
+
+	/**
+	 * Constructs an {@code OAuth2TokenExchangeAuthenticationProvider} using the provided
+	 * parameters.
+	 * @param authorizationService the authorization service
+	 * @param tokenGenerator the token generator
+	 */
+	public OAuth2TokenExchangeAuthenticationProvider(OAuth2AuthorizationService authorizationService,
+			OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
+		this.authorizationService = authorizationService;
+		this.tokenGenerator = tokenGenerator;
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2TokenExchangeAuthenticationToken tokenExchangeAuthentication = (OAuth2TokenExchangeAuthenticationToken) authentication;
+
+		OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils
+			.getAuthenticatedClientElseThrowInvalidClient(tokenExchangeAuthentication);
+		RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved registered client");
+		}
+
+		if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.TOKEN_EXCHANGE)) {
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
+		}
+
+		if (JWT_TOKEN_TYPE_VALUE.equals(tokenExchangeAuthentication.getRequestedTokenType())
+				&& !OAuth2TokenFormat.SELF_CONTAINED
+					.equals(registeredClient.getTokenSettings().getAccessTokenFormat())) {
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
+		}
+
+		OAuth2Authorization subjectAuthorization = this.authorizationService
+			.findByToken(tokenExchangeAuthentication.getSubjectToken(), OAuth2TokenType.ACCESS_TOKEN);
+		if (subjectAuthorization == null) {
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved authorization with subject token");
+		}
+
+		OAuth2Authorization.Token<OAuth2Token> subjectToken = subjectAuthorization
+			.getToken(tokenExchangeAuthentication.getSubjectToken());
+		if (!subjectToken.isActive()) {
+			// As per https://tools.ietf.org/html/rfc6749#section-5.2
+			// invalid_grant: The provided authorization grant (e.g., authorization code,
+			// resource owner credentials) or refresh token is invalid, expired, revoked
+			// [...].
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		if (!isValidTokenType(tokenExchangeAuthentication.getSubjectTokenType(), subjectToken)) {
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
+		}
+
+		if (subjectAuthorization.getAttribute(Principal.class.getName()) == null) {
+			// As per https://datatracker.ietf.org/doc/html/rfc8693#section-1.1,
+			// we require a principal to be available via the subject_token for
+			// impersonation or delegation use cases.
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		// As per https://datatracker.ietf.org/doc/html/rfc8693#section-4.4,
+		// The may_act claim makes a statement that one party is authorized to
+		// become the actor and act on behalf of another party.
+		Map<String, Object> authorizedActorClaims = null;
+		if (subjectToken.getClaims() != null && subjectToken.getClaims().containsKey(MAY_ACT)
+				&& subjectToken.getClaims().get(MAY_ACT) instanceof Map<?, ?> mayAct) {
+			authorizedActorClaims = (Map<String, Object>) mayAct;
+		}
+
+		OAuth2Authorization actorAuthorization = null;
+		if (StringUtils.hasText(tokenExchangeAuthentication.getActorToken())) {
+			actorAuthorization = this.authorizationService.findByToken(tokenExchangeAuthentication.getActorToken(),
+					OAuth2TokenType.ACCESS_TOKEN);
+			if (actorAuthorization == null) {
+				throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+			}
+
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Retrieved authorization with actor token");
+			}
+
+			OAuth2Authorization.Token<OAuth2Token> actorToken = actorAuthorization
+				.getToken(tokenExchangeAuthentication.getActorToken());
+			if (!actorToken.isActive()) {
+				// As per https://tools.ietf.org/html/rfc6749#section-5.2
+				// invalid_grant: The provided authorization grant (e.g., authorization
+				// code,
+				// resource owner credentials) or refresh token is invalid, expired,
+				// revoked [...].
+				throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+			}
+
+			if (!isValidTokenType(tokenExchangeAuthentication.getActorTokenType(), actorToken)) {
+				throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
+			}
+
+			if (authorizedActorClaims != null) {
+				validateClaims(authorizedActorClaims, actorToken.getClaims(), OAuth2TokenClaimNames.ISS,
+						OAuth2TokenClaimNames.SUB);
+			}
+		}
+		else if (authorizedActorClaims != null) {
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		Set<String> authorizedScopes = Collections.emptySet();
+		if (!CollectionUtils.isEmpty(tokenExchangeAuthentication.getScopes())) {
+			authorizedScopes = validateRequestedScopes(registeredClient, tokenExchangeAuthentication.getScopes());
+		}
+		else if (!CollectionUtils.isEmpty(subjectAuthorization.getAuthorizedScopes())) {
+			authorizedScopes = validateRequestedScopes(registeredClient, subjectAuthorization.getAuthorizedScopes());
+		}
+
+		// Verify the DPoP Proof (if available)
+		Jwt dPoPProof = DPoPProofVerifier.verifyIfAvailable(tokenExchangeAuthentication);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Validated token request parameters");
+		}
+
+		Authentication principal = getPrincipal(subjectAuthorization, actorAuthorization);
+
+		// @formatter:off
+		DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
+				.registeredClient(registeredClient)
+				.authorization(subjectAuthorization)
+				.principal(principal)
+				.authorizationServerContext(AuthorizationServerContextHolder.getContext())
+				.authorizedScopes(authorizedScopes)
+				.tokenType(OAuth2TokenType.ACCESS_TOKEN)
+				.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
+				.authorizationGrant(tokenExchangeAuthentication);
+		// @formatter:on
+		if (dPoPProof != null) {
+			tokenContextBuilder.put(OAuth2TokenContext.DPOP_PROOF_KEY, dPoPProof);
+		}
+
+		// ----- Access token -----
+		OAuth2TokenContext tokenContext = tokenContextBuilder.build();
+		OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
+		if (generatedAccessToken == null) {
+			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
+					"The token generator failed to generate the access token.", ERROR_URI);
+			throw new OAuth2AuthenticationException(error);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Generated access token");
+		}
+
+		// @formatter:off
+		OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
+				.principalName(subjectAuthorization.getPrincipalName())
+				.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
+				.authorizedScopes(authorizedScopes)
+				.attribute(Principal.class.getName(), principal);
+		// @formatter:on
+
+		OAuth2AccessToken accessToken = OAuth2AuthenticationProviderUtils.accessToken(authorizationBuilder,
+				generatedAccessToken, tokenContext);
+
+		OAuth2Authorization authorization = authorizationBuilder.build();
+		this.authorizationService.save(authorization);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Saved authorization");
+		}
+
+		Map<String, Object> additionalParameters = new HashMap<>();
+		additionalParameters.put(OAuth2ParameterNames.ISSUED_TOKEN_TYPE,
+				tokenExchangeAuthentication.getRequestedTokenType());
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Authenticated token request");
+		}
+
+		return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, null,
+				additionalParameters);
+	}
+
+	private static boolean isValidTokenType(String tokenType, OAuth2Authorization.Token<OAuth2Token> token) {
+		String tokenFormat = token.getMetadata(OAuth2TokenFormat.class.getName());
+		return ACCESS_TOKEN_TYPE_VALUE.equals(tokenType) || JWT_TOKEN_TYPE_VALUE.equals(tokenType)
+				&& OAuth2TokenFormat.SELF_CONTAINED.getValue().equals(tokenFormat);
+	}
+
+	private static Set<String> validateRequestedScopes(RegisteredClient registeredClient, Set<String> requestedScopes) {
+		for (String requestedScope : requestedScopes) {
+			if (!registeredClient.getScopes().contains(requestedScope)) {
+				throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
+			}
+		}
+
+		return new LinkedHashSet<>(requestedScopes);
+	}
+
+	private static void validateClaims(Map<String, Object> expectedClaims, Map<String, Object> actualClaims,
+			String... claimNames) {
+		if (actualClaims == null) {
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		for (String claimName : claimNames) {
+			if (!Objects.equals(expectedClaims.get(claimName), actualClaims.get(claimName))) {
+				throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+			}
+		}
+	}
+
+	private static Authentication getPrincipal(OAuth2Authorization subjectAuthorization,
+			OAuth2Authorization actorAuthorization) {
+		Authentication subjectPrincipal = subjectAuthorization.getAttribute(Principal.class.getName());
+		if (actorAuthorization == null) {
+			if (subjectPrincipal instanceof OAuth2TokenExchangeCompositeAuthenticationToken compositeAuthenticationToken) {
+				return compositeAuthenticationToken.getSubject();
+			}
+			return subjectPrincipal;
+		}
+
+		// Capture claims for current actor's access token
+		OAuth2TokenExchangeActor currentActor = new OAuth2TokenExchangeActor(
+				actorAuthorization.getAccessToken().getClaims());
+		List<OAuth2TokenExchangeActor> actorPrincipals = new LinkedList<>();
+		actorPrincipals.add(currentActor);
+
+		// Add chain of delegation for previous actor(s) if any
+		if (subjectPrincipal instanceof OAuth2TokenExchangeCompositeAuthenticationToken compositeAuthenticationToken) {
+			subjectPrincipal = compositeAuthenticationToken.getSubject();
+			actorPrincipals.addAll(compositeAuthenticationToken.getActors());
+		}
+
+		return new OAuth2TokenExchangeCompositeAuthenticationToken(subjectPrincipal, actorPrincipals);
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2TokenExchangeAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+}

+ 153 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeAuthenticationToken.java

@@ -0,0 +1,153 @@
+/*
+ * Copyright 2020-2024 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.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} implementation used for the OAuth 2.0 Token Exchange Grant.
+ *
+ * @author Steve Riesenberg
+ * @since 1.3
+ * @see OAuth2AuthorizationGrantAuthenticationToken
+ * @see OAuth2TokenExchangeAuthenticationProvider
+ */
+public class OAuth2TokenExchangeAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
+
+	private final String requestedTokenType;
+
+	private final String subjectToken;
+
+	private final String subjectTokenType;
+
+	private final String actorToken;
+
+	private final String actorTokenType;
+
+	private final Set<String> resources;
+
+	private final Set<String> audiences;
+
+	private final Set<String> scopes;
+
+	/**
+	 * Constructs an {@code OAuth2TokenExchangeAuthenticationToken} using the provided
+	 * parameters.
+	 * @param requestedTokenType the requested token type
+	 * @param subjectToken the subject token
+	 * @param subjectTokenType the subject token type
+	 * @param clientPrincipal the authenticated client principal
+	 * @param actorToken the actor token
+	 * @param actorTokenType the actor token type
+	 * @param resources the requested resource URI(s)
+	 * @param audiences the requested audience value(s)
+	 * @param scopes the requested scope(s)
+	 * @param additionalParameters the additional parameters
+	 */
+	public OAuth2TokenExchangeAuthenticationToken(String requestedTokenType, String subjectToken,
+			String subjectTokenType, Authentication clientPrincipal, @Nullable String actorToken,
+			@Nullable String actorTokenType, @Nullable Set<String> resources, @Nullable Set<String> audiences,
+			@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
+		super(AuthorizationGrantType.TOKEN_EXCHANGE, clientPrincipal, additionalParameters);
+		Assert.hasText(requestedTokenType, "requestedTokenType cannot be empty");
+		Assert.hasText(subjectToken, "subjectToken cannot be empty");
+		Assert.hasText(subjectTokenType, "subjectTokenType cannot be empty");
+		this.requestedTokenType = requestedTokenType;
+		this.subjectToken = subjectToken;
+		this.subjectTokenType = subjectTokenType;
+		this.actorToken = actorToken;
+		this.actorTokenType = actorTokenType;
+		this.resources = Collections
+			.unmodifiableSet((resources != null) ? new LinkedHashSet<>(resources) : Collections.emptySet());
+		this.audiences = Collections
+			.unmodifiableSet((audiences != null) ? new LinkedHashSet<>(audiences) : Collections.emptySet());
+		this.scopes = Collections.unmodifiableSet((scopes != null) ? new HashSet<>(scopes) : Collections.emptySet());
+	}
+
+	/**
+	 * Returns the requested token type.
+	 * @return the requested token type
+	 */
+	public String getRequestedTokenType() {
+		return this.requestedTokenType;
+	}
+
+	/**
+	 * Returns the subject token.
+	 * @return the subject token
+	 */
+	public String getSubjectToken() {
+		return this.subjectToken;
+	}
+
+	/**
+	 * Returns the subject token type.
+	 * @return the subject token type
+	 */
+	public String getSubjectTokenType() {
+		return this.subjectTokenType;
+	}
+
+	/**
+	 * Returns the actor token.
+	 * @return the actor token
+	 */
+	public String getActorToken() {
+		return this.actorToken;
+	}
+
+	/**
+	 * Returns the actor token type.
+	 * @return the actor token type
+	 */
+	public String getActorTokenType() {
+		return this.actorTokenType;
+	}
+
+	/**
+	 * Returns the requested resource URI(s).
+	 * @return the requested resource URI(s), or an empty {@code Set} if not available
+	 */
+	public Set<String> getResources() {
+		return this.resources;
+	}
+
+	/**
+	 * Returns the requested audience value(s).
+	 * @return the requested audience value(s), or an empty {@code Set} if not available
+	 */
+	public Set<String> getAudiences() {
+		return this.audiences;
+	}
+
+	/**
+	 * Returns the requested scope(s).
+	 * @return the requested scope(s), or an empty {@code Set} if not available
+	 */
+	public Set<String> getScopes() {
+		return this.scopes;
+	}
+
+}

+ 85 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeCompositeAuthenticationToken.java

@@ -0,0 +1,85 @@
+/*
+ * Copyright 2020-2024 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.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} implementation used for the OAuth 2.0 Token Exchange Grant to
+ * represent the principal in a composite token (e.g. the "delegation" use case).
+ *
+ * @author Steve Riesenberg
+ * @since 1.3
+ * @see OAuth2TokenExchangeAuthenticationToken
+ */
+public class OAuth2TokenExchangeCompositeAuthenticationToken extends AbstractAuthenticationToken {
+
+	private final Authentication subject;
+
+	private final List<OAuth2TokenExchangeActor> actors;
+
+	public OAuth2TokenExchangeCompositeAuthenticationToken(Authentication subject,
+			List<OAuth2TokenExchangeActor> actors) {
+		super((subject != null) ? subject.getAuthorities() : null);
+		Assert.notNull(subject, "subject cannot be null");
+		Assert.notNull(actors, "actors cannot be null");
+		this.subject = subject;
+		this.actors = Collections.unmodifiableList(new ArrayList<>(actors));
+		setDetails(subject.getDetails());
+		setAuthenticated(subject.isAuthenticated());
+	}
+
+	@Override
+	public Object getPrincipal() {
+		return this.subject.getPrincipal();
+	}
+
+	@Override
+	public Object getCredentials() {
+		return null;
+	}
+
+	public Authentication getSubject() {
+		return this.subject;
+	}
+
+	public List<OAuth2TokenExchangeActor> getActors() {
+		return this.actors;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (!(obj instanceof OAuth2TokenExchangeCompositeAuthenticationToken other)) {
+			return false;
+		}
+		return super.equals(obj) && Objects.equals(this.subject, other.subject)
+				&& Objects.equals(this.actors, other.actors);
+	}
+
+	@Override
+	public int hashCode() {
+		return Objects.hash(super.hashCode(), this.subject, this.actors);
+	}
+
+}

+ 193 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProvider.java

@@ -0,0 +1,193 @@
+/*
+ * 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.net.URL;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.convert.TypeDescriptor;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2Token;
+import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
+import org.springframework.security.oauth2.core.converter.ClaimConversionService;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenIntrospection;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * An {@link AuthenticationProvider} implementation for OAuth 2.0 Token Introspection.
+ *
+ * @author Gerardo Roza
+ * @author Joe Grandja
+ * @since 0.1.1
+ * @see OAuth2TokenIntrospectionAuthenticationToken
+ * @see RegisteredClientRepository
+ * @see OAuth2AuthorizationService
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7662#section-2.1">Section
+ * 2.1 Introspection Request</a>
+ */
+public final class OAuth2TokenIntrospectionAuthenticationProvider implements AuthenticationProvider {
+
+	private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
+
+	private static final TypeDescriptor LIST_STRING_TYPE_DESCRIPTOR = TypeDescriptor.collection(List.class,
+			TypeDescriptor.valueOf(String.class));
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final RegisteredClientRepository registeredClientRepository;
+
+	private final OAuth2AuthorizationService authorizationService;
+
+	/**
+	 * Constructs an {@code OAuth2TokenIntrospectionAuthenticationProvider} using the
+	 * provided parameters.
+	 * @param registeredClientRepository the repository of registered clients
+	 * @param authorizationService the authorization service
+	 */
+	public OAuth2TokenIntrospectionAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
+			OAuth2AuthorizationService authorizationService) {
+		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		this.registeredClientRepository = registeredClientRepository;
+		this.authorizationService = authorizationService;
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = (OAuth2TokenIntrospectionAuthenticationToken) authentication;
+
+		OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils
+			.getAuthenticatedClientElseThrowInvalidClient(tokenIntrospectionAuthentication);
+
+		OAuth2Authorization authorization = this.authorizationService
+			.findByToken(tokenIntrospectionAuthentication.getToken(), null);
+		if (authorization == null) {
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Did not authenticate token introspection request since token was not found");
+			}
+			// Return the authentication request when token not found
+			return tokenIntrospectionAuthentication;
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved authorization with token");
+		}
+
+		OAuth2Authorization.Token<OAuth2Token> authorizedToken = authorization
+			.getToken(tokenIntrospectionAuthentication.getToken());
+		if (!authorizedToken.isActive()) {
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Did not introspect token since not active");
+			}
+			return new OAuth2TokenIntrospectionAuthenticationToken(tokenIntrospectionAuthentication.getToken(),
+					clientPrincipal, OAuth2TokenIntrospection.builder().build());
+		}
+
+		RegisteredClient authorizedClient = this.registeredClientRepository
+			.findById(authorization.getRegisteredClientId());
+		OAuth2TokenIntrospection tokenClaims = withActiveTokenClaims(authorizedToken, authorizedClient);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Authenticated token introspection request");
+		}
+
+		return new OAuth2TokenIntrospectionAuthenticationToken(authorizedToken.getToken().getTokenValue(),
+				clientPrincipal, tokenClaims);
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2TokenIntrospectionAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	private static OAuth2TokenIntrospection withActiveTokenClaims(
+			OAuth2Authorization.Token<OAuth2Token> authorizedToken, RegisteredClient authorizedClient) {
+
+		OAuth2TokenIntrospection.Builder tokenClaims;
+		if (!CollectionUtils.isEmpty(authorizedToken.getClaims())) {
+			Map<String, Object> claims = convertClaimsIfNecessary(authorizedToken.getClaims());
+			tokenClaims = OAuth2TokenIntrospection.withClaims(claims).active(true);
+		}
+		else {
+			tokenClaims = OAuth2TokenIntrospection.builder(true);
+		}
+
+		tokenClaims.clientId(authorizedClient.getClientId());
+
+		// TODO Set "username"
+
+		OAuth2Token token = authorizedToken.getToken();
+		if (token.getIssuedAt() != null) {
+			tokenClaims.issuedAt(token.getIssuedAt());
+		}
+		if (token.getExpiresAt() != null) {
+			tokenClaims.expiresAt(token.getExpiresAt());
+		}
+
+		if (OAuth2AccessToken.class.isAssignableFrom(token.getClass())) {
+			OAuth2AccessToken accessToken = (OAuth2AccessToken) token;
+			tokenClaims.tokenType(accessToken.getTokenType().getValue());
+		}
+
+		return tokenClaims.build();
+	}
+
+	private static Map<String, Object> convertClaimsIfNecessary(Map<String, Object> claims) {
+		Map<String, Object> convertedClaims = new HashMap<>(claims);
+
+		Object value = claims.get(OAuth2TokenIntrospectionClaimNames.ISS);
+		if (value != null && !(value instanceof URL)) {
+			URL convertedValue = ClaimConversionService.getSharedInstance().convert(value, URL.class);
+			if (convertedValue != null) {
+				convertedClaims.put(OAuth2TokenIntrospectionClaimNames.ISS, convertedValue);
+			}
+		}
+
+		value = claims.get(OAuth2TokenIntrospectionClaimNames.SCOPE);
+		if (value != null && !(value instanceof List)) {
+			Object convertedValue = ClaimConversionService.getSharedInstance()
+				.convert(value, OBJECT_TYPE_DESCRIPTOR, LIST_STRING_TYPE_DESCRIPTOR);
+			if (convertedValue != null) {
+				convertedClaims.put(OAuth2TokenIntrospectionClaimNames.SCOPE, convertedValue);
+			}
+		}
+
+		value = claims.get(OAuth2TokenIntrospectionClaimNames.AUD);
+		if (value != null && !(value instanceof List)) {
+			Object convertedValue = ClaimConversionService.getSharedInstance()
+				.convert(value, OBJECT_TYPE_DESCRIPTOR, LIST_STRING_TYPE_DESCRIPTOR);
+			if (convertedValue != null) {
+				convertedClaims.put(OAuth2TokenIntrospectionClaimNames.AUD, convertedValue);
+			}
+		}
+
+		return convertedClaims;
+	}
+
+}

+ 141 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationToken.java

@@ -0,0 +1,141 @@
+/*
+ * Copyright 2020-2025 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.io.Serial;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenIntrospection;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} implementation used for OAuth 2.0 Token Introspection.
+ *
+ * @author Gerardo Roza
+ * @author Joe Grandja
+ * @since 0.1.1
+ * @see AbstractAuthenticationToken
+ * @see OAuth2TokenIntrospection
+ * @see OAuth2TokenIntrospectionAuthenticationProvider
+ */
+public class OAuth2TokenIntrospectionAuthenticationToken extends AbstractAuthenticationToken {
+
+	@Serial
+	private static final long serialVersionUID = 9003173975452760956L;
+
+	private final String token;
+
+	private final Authentication clientPrincipal;
+
+	private final String tokenTypeHint;
+
+	private final Map<String, Object> additionalParameters;
+
+	private final OAuth2TokenIntrospection tokenClaims;
+
+	/**
+	 * Constructs an {@code OAuth2TokenIntrospectionAuthenticationToken} using the
+	 * provided parameters.
+	 * @param token the token
+	 * @param clientPrincipal the authenticated client principal
+	 * @param tokenTypeHint the token type hint
+	 * @param additionalParameters the additional parameters
+	 */
+	public OAuth2TokenIntrospectionAuthenticationToken(String token, Authentication clientPrincipal,
+			@Nullable String tokenTypeHint, @Nullable Map<String, Object> additionalParameters) {
+		super(Collections.emptyList());
+		Assert.hasText(token, "token cannot be empty");
+		Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
+		this.token = token;
+		this.clientPrincipal = clientPrincipal;
+		this.tokenTypeHint = tokenTypeHint;
+		this.additionalParameters = Collections.unmodifiableMap(
+				(additionalParameters != null) ? new HashMap<>(additionalParameters) : Collections.emptyMap());
+		this.tokenClaims = OAuth2TokenIntrospection.builder().build();
+	}
+
+	/**
+	 * Constructs an {@code OAuth2TokenIntrospectionAuthenticationToken} using the
+	 * provided parameters.
+	 * @param token the token
+	 * @param clientPrincipal the authenticated client principal
+	 * @param tokenClaims the token claims
+	 */
+	public OAuth2TokenIntrospectionAuthenticationToken(String token, Authentication clientPrincipal,
+			OAuth2TokenIntrospection tokenClaims) {
+		super(Collections.emptyList());
+		Assert.hasText(token, "token cannot be empty");
+		Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
+		Assert.notNull(tokenClaims, "tokenClaims cannot be null");
+		this.token = token;
+		this.clientPrincipal = clientPrincipal;
+		this.tokenTypeHint = null;
+		this.additionalParameters = Collections.emptyMap();
+		this.tokenClaims = tokenClaims;
+		// Indicates that the request was authenticated, even though the token might not
+		// be active
+		setAuthenticated(true);
+	}
+
+	@Override
+	public Object getPrincipal() {
+		return this.clientPrincipal;
+	}
+
+	@Override
+	public Object getCredentials() {
+		return "";
+	}
+
+	/**
+	 * Returns the token.
+	 * @return the token
+	 */
+	public String getToken() {
+		return this.token;
+	}
+
+	/**
+	 * Returns the token type hint.
+	 * @return the token type hint
+	 */
+	@Nullable
+	public String getTokenTypeHint() {
+		return this.tokenTypeHint;
+	}
+
+	/**
+	 * Returns the additional parameters.
+	 * @return the additional parameters
+	 */
+	public Map<String, Object> getAdditionalParameters() {
+		return this.additionalParameters;
+	}
+
+	/**
+	 * Returns the token claims.
+	 * @return the {@link OAuth2TokenIntrospection}
+	 */
+	public OAuth2TokenIntrospection getTokenClaims() {
+		return this.tokenClaims;
+	}
+
+}

+ 99 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenRevocationAuthenticationProvider.java

@@ -0,0 +1,99 @@
+/*
+ * Copyright 2020-2024 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.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.OAuth2Token;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link AuthenticationProvider} implementation for OAuth 2.0 Token Revocation.
+ *
+ * @author Vivek Babu
+ * @author Joe Grandja
+ * @since 0.0.3
+ * @see OAuth2TokenRevocationAuthenticationToken
+ * @see OAuth2AuthorizationService
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7009#section-2.1">Section
+ * 2.1 Revocation Request</a>
+ */
+public final class OAuth2TokenRevocationAuthenticationProvider implements AuthenticationProvider {
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final OAuth2AuthorizationService authorizationService;
+
+	/**
+	 * Constructs an {@code OAuth2TokenRevocationAuthenticationProvider} using the
+	 * provided parameters.
+	 * @param authorizationService the authorization service
+	 */
+	public OAuth2TokenRevocationAuthenticationProvider(OAuth2AuthorizationService authorizationService) {
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		this.authorizationService = authorizationService;
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2TokenRevocationAuthenticationToken tokenRevocationAuthentication = (OAuth2TokenRevocationAuthenticationToken) authentication;
+
+		OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils
+			.getAuthenticatedClientElseThrowInvalidClient(tokenRevocationAuthentication);
+		RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
+
+		OAuth2Authorization authorization = this.authorizationService
+			.findByToken(tokenRevocationAuthentication.getToken(), null);
+		if (authorization == null) {
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Did not authenticate token revocation request since token was not found");
+			}
+			// Return the authentication request when token not found
+			return tokenRevocationAuthentication;
+		}
+
+		if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
+		}
+
+		OAuth2Authorization.Token<OAuth2Token> token = authorization.getToken(tokenRevocationAuthentication.getToken());
+		authorization = OAuth2Authorization.from(authorization).invalidate(token.getToken()).build();
+		this.authorizationService.save(authorization);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Saved authorization with revoked token");
+			// This log is kept separate for consistency with other providers
+			this.logger.trace("Authenticated token revocation request");
+		}
+
+		return new OAuth2TokenRevocationAuthenticationToken(token.getToken(), clientPrincipal);
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2TokenRevocationAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+}

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

@@ -0,0 +1,107 @@
+/*
+ * Copyright 2020-2025 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.io.Serial;
+import java.util.Collections;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.OAuth2Token;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} implementation used for OAuth 2.0 Token Revocation.
+ *
+ * @author Vivek Babu
+ * @author Joe Grandja
+ * @since 0.0.3
+ * @see AbstractAuthenticationToken
+ * @see OAuth2TokenRevocationAuthenticationProvider
+ */
+public class OAuth2TokenRevocationAuthenticationToken extends AbstractAuthenticationToken {
+
+	@Serial
+	private static final long serialVersionUID = -880609099230203249L;
+
+	private final String token;
+
+	private final Authentication clientPrincipal;
+
+	private final String tokenTypeHint;
+
+	/**
+	 * Constructs an {@code OAuth2TokenRevocationAuthenticationToken} using the provided
+	 * parameters.
+	 * @param token the token
+	 * @param clientPrincipal the authenticated client principal
+	 * @param tokenTypeHint the token type hint
+	 */
+	public OAuth2TokenRevocationAuthenticationToken(String token, Authentication clientPrincipal,
+			@Nullable String tokenTypeHint) {
+		super(Collections.emptyList());
+		Assert.hasText(token, "token cannot be empty");
+		Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
+		this.token = token;
+		this.clientPrincipal = clientPrincipal;
+		this.tokenTypeHint = tokenTypeHint;
+	}
+
+	/**
+	 * Constructs an {@code OAuth2TokenRevocationAuthenticationToken} using the provided
+	 * parameters.
+	 * @param revokedToken the revoked token
+	 * @param clientPrincipal the authenticated client principal
+	 */
+	public OAuth2TokenRevocationAuthenticationToken(OAuth2Token revokedToken, Authentication clientPrincipal) {
+		super(Collections.emptyList());
+		Assert.notNull(revokedToken, "revokedToken cannot be null");
+		Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
+		this.token = revokedToken.getTokenValue();
+		this.clientPrincipal = clientPrincipal;
+		this.tokenTypeHint = null;
+		setAuthenticated(true); // Indicates that the token was authenticated and revoked
+	}
+
+	@Override
+	public Object getPrincipal() {
+		return this.clientPrincipal;
+	}
+
+	@Override
+	public Object getCredentials() {
+		return "";
+	}
+
+	/**
+	 * Returns the token.
+	 * @return the token
+	 */
+	public String getToken() {
+		return this.token;
+	}
+
+	/**
+	 * Returns the token type hint.
+	 * @return the token type hint
+	 */
+	@Nullable
+	public String getTokenTypeHint() {
+		return this.tokenTypeHint;
+	}
+
+}

+ 38 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OidcPrompt.java

@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020-2025 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;
+
+/**
+ * The values defined for the "prompt" parameter for the OpenID Connect 1.0 Authentication
+ * Request.
+ *
+ * @author Joe Grandja
+ * @since 1.5
+ */
+final class OidcPrompt {
+
+	static final String NONE = "none";
+
+	static final String LOGIN = "login";
+
+	static final String CONSENT = "consent";
+
+	static final String SELECT_ACCOUNT = "select_account";
+
+	private OidcPrompt() {
+	}
+
+}

+ 120 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/PublicClientAuthenticationProvider.java

@@ -0,0 +1,120 @@
+/*
+ * 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.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+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.endpoint.PkceParameterNames;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link AuthenticationProvider} implementation used for OAuth 2.0 Public Client
+ * Authentication, which authenticates the {@link PkceParameterNames#CODE_VERIFIER
+ * code_verifier} parameter.
+ *
+ * @author Joe Grandja
+ * @since 0.2.3
+ * @see AuthenticationProvider
+ * @see OAuth2ClientAuthenticationToken
+ * @see RegisteredClientRepository
+ * @see OAuth2AuthorizationService
+ */
+public final class PublicClientAuthenticationProvider implements AuthenticationProvider {
+
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final RegisteredClientRepository registeredClientRepository;
+
+	private final CodeVerifierAuthenticator codeVerifierAuthenticator;
+
+	/**
+	 * Constructs a {@code PublicClientAuthenticationProvider} using the provided
+	 * parameters.
+	 * @param registeredClientRepository the repository of registered clients
+	 * @param authorizationService the authorization service
+	 */
+	public PublicClientAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
+			OAuth2AuthorizationService authorizationService) {
+		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		this.registeredClientRepository = registeredClientRepository;
+		this.codeVerifierAuthenticator = new CodeVerifierAuthenticator(authorizationService);
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) authentication;
+
+		if (!ClientAuthenticationMethod.NONE.equals(clientAuthentication.getClientAuthenticationMethod())) {
+			return null;
+		}
+
+		String clientId = clientAuthentication.getPrincipal().toString();
+		RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
+		if (registeredClient == null) {
+			throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved registered client");
+		}
+
+		if (!registeredClient.getClientAuthenticationMethods()
+			.contains(clientAuthentication.getClientAuthenticationMethod())) {
+			throwInvalidClient("authentication_method");
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Validated client authentication parameters");
+		}
+
+		// Validate the "code_verifier" parameter for the public client
+		this.codeVerifierAuthenticator.authenticateRequired(clientAuthentication, registeredClient);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Authenticated public client");
+		}
+
+		return new OAuth2ClientAuthenticationToken(registeredClient,
+				clientAuthentication.getClientAuthenticationMethod(), null);
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	private static void throwInvalidClient(String parameterName) {
+		OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
+				"Client authentication failed: " + parameterName, ERROR_URI);
+		throw new OAuth2AuthenticationException(error);
+	}
+
+}

+ 190 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/X509ClientCertificateAuthenticationProvider.java

@@ -0,0 +1,190 @@
+/*
+ * Copyright 2020-2024 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.security.cert.X509Certificate;
+import java.util.function.Consumer;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+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.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * An {@link AuthenticationProvider} implementation used for OAuth 2.0 Client
+ * Authentication, which authenticates the client {@code X509Certificate} received when
+ * the {@code tls_client_auth} or {@code self_signed_tls_client_auth} authentication
+ * method is used.
+ *
+ * @author Joe Grandja
+ * @since 1.3
+ * @see AuthenticationProvider
+ * @see OAuth2ClientAuthenticationToken
+ * @see RegisteredClientRepository
+ * @see OAuth2AuthorizationService
+ */
+public final class X509ClientCertificateAuthenticationProvider implements AuthenticationProvider {
+
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final RegisteredClientRepository registeredClientRepository;
+
+	private final CodeVerifierAuthenticator codeVerifierAuthenticator;
+
+	private final Consumer<OAuth2ClientAuthenticationContext> selfSignedCertificateVerifier = new X509SelfSignedCertificateVerifier();
+
+	private Consumer<OAuth2ClientAuthenticationContext> certificateVerifier = this::verifyX509Certificate;
+
+	/**
+	 * Constructs a {@code X509ClientCertificateAuthenticationProvider} using the provided
+	 * parameters.
+	 * @param registeredClientRepository the repository of registered clients
+	 * @param authorizationService the authorization service
+	 */
+	public X509ClientCertificateAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
+			OAuth2AuthorizationService authorizationService) {
+		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		this.registeredClientRepository = registeredClientRepository;
+		this.codeVerifierAuthenticator = new CodeVerifierAuthenticator(authorizationService);
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) authentication;
+
+		if (!ClientAuthenticationMethod.TLS_CLIENT_AUTH.equals(clientAuthentication.getClientAuthenticationMethod())
+				&& !ClientAuthenticationMethod.SELF_SIGNED_TLS_CLIENT_AUTH
+					.equals(clientAuthentication.getClientAuthenticationMethod())) {
+			return null;
+		}
+
+		String clientId = clientAuthentication.getPrincipal().toString();
+		RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
+		if (registeredClient == null) {
+			throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved registered client");
+		}
+
+		if (!registeredClient.getClientAuthenticationMethods()
+			.contains(clientAuthentication.getClientAuthenticationMethod())) {
+			throwInvalidClient("authentication_method");
+		}
+
+		if (!(clientAuthentication.getCredentials() instanceof X509Certificate[])) {
+			throwInvalidClient("credentials");
+		}
+
+		OAuth2ClientAuthenticationContext authenticationContext = OAuth2ClientAuthenticationContext
+			.with(clientAuthentication)
+			.registeredClient(registeredClient)
+			.build();
+		this.certificateVerifier.accept(authenticationContext);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Validated client authentication parameters");
+		}
+
+		// Validate the "code_verifier" parameter for the confidential client, if
+		// available
+		this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Authenticated client X509Certificate");
+		}
+
+		return new OAuth2ClientAuthenticationToken(registeredClient,
+				clientAuthentication.getClientAuthenticationMethod(), clientAuthentication.getCredentials());
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	/**
+	 * Sets the {@code Consumer} providing access to the
+	 * {@link OAuth2ClientAuthenticationContext} and is responsible for verifying the
+	 * client {@code X509Certificate} associated in the
+	 * {@link OAuth2ClientAuthenticationToken}. The default implementation for the
+	 * {@code tls_client_auth} authentication method verifies the
+	 * {@link ClientSettings#getX509CertificateSubjectDN() expected subject distinguished
+	 * name}.
+	 *
+	 * <p>
+	 * <b>NOTE:</b> If verification fails, an {@link OAuth2AuthenticationException} MUST
+	 * be thrown.
+	 * @param certificateVerifier the {@code Consumer} providing access to the
+	 * {@link OAuth2ClientAuthenticationContext} and is responsible for verifying the
+	 * client {@code X509Certificate}
+	 */
+	public void setCertificateVerifier(Consumer<OAuth2ClientAuthenticationContext> certificateVerifier) {
+		Assert.notNull(certificateVerifier, "certificateVerifier cannot be null");
+		this.certificateVerifier = certificateVerifier;
+	}
+
+	private void verifyX509Certificate(OAuth2ClientAuthenticationContext clientAuthenticationContext) {
+		OAuth2ClientAuthenticationToken clientAuthentication = clientAuthenticationContext.getAuthentication();
+		if (ClientAuthenticationMethod.SELF_SIGNED_TLS_CLIENT_AUTH
+			.equals(clientAuthentication.getClientAuthenticationMethod())) {
+			this.selfSignedCertificateVerifier.accept(clientAuthenticationContext);
+		}
+		else {
+			verifyX509CertificateSubjectDN(clientAuthenticationContext);
+		}
+	}
+
+	private void verifyX509CertificateSubjectDN(OAuth2ClientAuthenticationContext clientAuthenticationContext) {
+		OAuth2ClientAuthenticationToken clientAuthentication = clientAuthenticationContext.getAuthentication();
+		RegisteredClient registeredClient = clientAuthenticationContext.getRegisteredClient();
+		X509Certificate[] clientCertificateChain = (X509Certificate[]) clientAuthentication.getCredentials();
+		X509Certificate clientCertificate = clientCertificateChain[0];
+		String expectedSubjectDN = registeredClient.getClientSettings().getX509CertificateSubjectDN();
+		if (!StringUtils.hasText(expectedSubjectDN)
+				|| !clientCertificate.getSubjectX500Principal().getName().equals(expectedSubjectDN)) {
+			throwInvalidClient("x509_certificate_subject_dn");
+		}
+	}
+
+	private static void throwInvalidClient(String parameterName) {
+		throwInvalidClient(parameterName, null);
+	}
+
+	private static void throwInvalidClient(String parameterName, Throwable cause) {
+		OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
+				"Client authentication failed: " + parameterName, ERROR_URI);
+		throw new OAuth2AuthenticationException(error, error.toString(), cause);
+	}
+
+}

+ 223 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/X509SelfSignedCertificateVerifier.java

@@ -0,0 +1,223 @@
+/*
+ * Copyright 2020-2024 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.net.URI;
+import java.net.URISyntaxException;
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+import java.text.ParseException;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import javax.security.auth.x500.X500Principal;
+
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKMatcher;
+import com.nimbusds.jose.jwk.JWKSet;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.RequestEntity;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+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.server.authorization.client.RegisteredClient;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestOperations;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * The default {@code X509Certificate} verifier for the
+ * {@code self_signed_tls_client_auth} authentication method.
+ *
+ * @author Joe Grandja
+ * @since 1.3
+ * @see X509ClientCertificateAuthenticationProvider#setCertificateVerifier(Consumer)
+ */
+final class X509SelfSignedCertificateVerifier implements Consumer<OAuth2ClientAuthenticationContext> {
+
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
+
+	private static final JWKMatcher HAS_X509_CERT_CHAIN_MATCHER = new JWKMatcher.Builder().hasX509CertChain(true)
+		.build();
+
+	private final Function<RegisteredClient, JWKSet> jwkSetSupplier = new JwkSetSupplier();
+
+	@Override
+	public void accept(OAuth2ClientAuthenticationContext clientAuthenticationContext) {
+		OAuth2ClientAuthenticationToken clientAuthentication = clientAuthenticationContext.getAuthentication();
+		RegisteredClient registeredClient = clientAuthenticationContext.getRegisteredClient();
+		X509Certificate[] clientCertificateChain = (X509Certificate[]) clientAuthentication.getCredentials();
+		X509Certificate clientCertificate = clientCertificateChain[0];
+
+		X500Principal issuer = clientCertificate.getIssuerX500Principal();
+		X500Principal subject = clientCertificate.getSubjectX500Principal();
+		if (issuer == null || !issuer.equals(subject)) {
+			throwInvalidClient("x509_certificate_issuer");
+		}
+
+		JWKSet jwkSet = this.jwkSetSupplier.apply(registeredClient);
+
+		boolean publicKeyMatches = false;
+		for (JWK jwk : jwkSet.filter(HAS_X509_CERT_CHAIN_MATCHER).getKeys()) {
+			X509Certificate x509Certificate = jwk.getParsedX509CertChain().get(0);
+			PublicKey publicKey = x509Certificate.getPublicKey();
+			if (Arrays.equals(clientCertificate.getPublicKey().getEncoded(), publicKey.getEncoded())) {
+				publicKeyMatches = true;
+				break;
+			}
+		}
+
+		if (!publicKeyMatches) {
+			throwInvalidClient("x509_certificate");
+		}
+	}
+
+	private static void throwInvalidClient(String parameterName) {
+		throwInvalidClient(parameterName, null);
+	}
+
+	private static void throwInvalidClient(String parameterName, Throwable cause) {
+		OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
+				"Client authentication failed: " + parameterName, ERROR_URI);
+		throw new OAuth2AuthenticationException(error, error.toString(), cause);
+	}
+
+	private static final class JwkSetSupplier implements Function<RegisteredClient, JWKSet> {
+
+		private static final MediaType APPLICATION_JWK_SET_JSON = new MediaType("application", "jwk-set+json");
+
+		private final RestOperations restOperations;
+
+		private final Map<String, Supplier<JWKSet>> jwkSets = new ConcurrentHashMap<>();
+
+		private JwkSetSupplier() {
+			SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
+			requestFactory.setConnectTimeout(15_000);
+			requestFactory.setReadTimeout(15_000);
+			this.restOperations = new RestTemplate(requestFactory);
+		}
+
+		@Override
+		public JWKSet apply(RegisteredClient registeredClient) {
+			Supplier<JWKSet> jwkSetSupplier = this.jwkSets.computeIfAbsent(registeredClient.getId(), (key) -> {
+				if (!StringUtils.hasText(registeredClient.getClientSettings().getJwkSetUrl())) {
+					throwInvalidClient("client_jwk_set_url");
+				}
+				return new JwkSetHolder(registeredClient.getClientSettings().getJwkSetUrl());
+			});
+			return jwkSetSupplier.get();
+		}
+
+		private JWKSet retrieve(String jwkSetUrl) {
+			URI jwkSetUri = null;
+			try {
+				jwkSetUri = new URI(jwkSetUrl);
+			}
+			catch (URISyntaxException ex) {
+				throwInvalidClient("jwk_set_uri", ex);
+			}
+
+			HttpHeaders headers = new HttpHeaders();
+			headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON));
+			RequestEntity<Void> request = new RequestEntity<>(headers, HttpMethod.GET, jwkSetUri);
+			ResponseEntity<String> response = null;
+			try {
+				response = this.restOperations.exchange(request, String.class);
+			}
+			catch (Exception ex) {
+				throwInvalidClient("jwk_set_response_error", ex);
+			}
+			if (response.getStatusCode().value() != 200) {
+				throwInvalidClient("jwk_set_response_status");
+			}
+
+			JWKSet jwkSet = null;
+			try {
+				jwkSet = JWKSet.parse(response.getBody());
+			}
+			catch (ParseException ex) {
+				throwInvalidClient("jwk_set_response_body", ex);
+			}
+
+			return jwkSet;
+		}
+
+		private final class JwkSetHolder implements Supplier<JWKSet> {
+
+			private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
+
+			private final Clock clock = Clock.systemUTC();
+
+			private final String jwkSetUrl;
+
+			private JWKSet jwkSet;
+
+			private Instant lastUpdatedAt;
+
+			private JwkSetHolder(String jwkSetUrl) {
+				this.jwkSetUrl = jwkSetUrl;
+			}
+
+			@Override
+			public JWKSet get() {
+				this.rwLock.readLock().lock();
+				if (shouldRefresh()) {
+					this.rwLock.readLock().unlock();
+					this.rwLock.writeLock().lock();
+					try {
+						if (shouldRefresh()) {
+							this.jwkSet = retrieve(this.jwkSetUrl);
+							this.lastUpdatedAt = Instant.now();
+						}
+						this.rwLock.readLock().lock();
+					}
+					finally {
+						this.rwLock.writeLock().unlock();
+					}
+				}
+
+				try {
+					return this.jwkSet;
+				}
+				finally {
+					this.rwLock.readLock().unlock();
+				}
+			}
+
+			private boolean shouldRefresh() {
+				// Refresh every 5 minutes
+				return (this.jwkSet == null
+						|| this.clock.instant().isAfter(this.lastUpdatedAt.plus(5, ChronoUnit.MINUTES)));
+			}
+
+		}
+
+	}
+
+}

+ 118 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/InMemoryRegisteredClientRepository.java

@@ -0,0 +1,118 @@
+/*
+ * Copyright 2020-2023 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.client;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * A {@link RegisteredClientRepository} that stores {@link RegisteredClient}(s) in-memory.
+ *
+ * <p>
+ * <b>NOTE:</b> This implementation is recommended ONLY to be used during
+ * development/testing.
+ *
+ * @author Anoop Garlapati
+ * @author Ovidiu Popa
+ * @author Joe Grandja
+ * @since 0.0.1
+ * @see RegisteredClientRepository
+ * @see RegisteredClient
+ */
+public final class InMemoryRegisteredClientRepository implements RegisteredClientRepository {
+
+	private final Map<String, RegisteredClient> idRegistrationMap;
+
+	private final Map<String, RegisteredClient> clientIdRegistrationMap;
+
+	/**
+	 * Constructs an {@code InMemoryRegisteredClientRepository} using the provided
+	 * parameters.
+	 * @param registrations the client registration(s)
+	 */
+	public InMemoryRegisteredClientRepository(RegisteredClient... registrations) {
+		this(Arrays.asList(registrations));
+	}
+
+	/**
+	 * Constructs an {@code InMemoryRegisteredClientRepository} using the provided
+	 * parameters.
+	 * @param registrations the client registration(s)
+	 */
+	public InMemoryRegisteredClientRepository(List<RegisteredClient> registrations) {
+		Assert.notEmpty(registrations, "registrations cannot be empty");
+		ConcurrentHashMap<String, RegisteredClient> idRegistrationMapResult = new ConcurrentHashMap<>();
+		ConcurrentHashMap<String, RegisteredClient> clientIdRegistrationMapResult = new ConcurrentHashMap<>();
+		for (RegisteredClient registration : registrations) {
+			Assert.notNull(registration, "registration cannot be null");
+			assertUniqueIdentifiers(registration, idRegistrationMapResult);
+			idRegistrationMapResult.put(registration.getId(), registration);
+			clientIdRegistrationMapResult.put(registration.getClientId(), registration);
+		}
+		this.idRegistrationMap = idRegistrationMapResult;
+		this.clientIdRegistrationMap = clientIdRegistrationMapResult;
+	}
+
+	@Override
+	public void save(RegisteredClient registeredClient) {
+		Assert.notNull(registeredClient, "registeredClient cannot be null");
+		if (!this.idRegistrationMap.containsKey(registeredClient.getId())) {
+			assertUniqueIdentifiers(registeredClient, this.idRegistrationMap);
+		}
+		this.idRegistrationMap.put(registeredClient.getId(), registeredClient);
+		this.clientIdRegistrationMap.put(registeredClient.getClientId(), registeredClient);
+	}
+
+	@Nullable
+	@Override
+	public RegisteredClient findById(String id) {
+		Assert.hasText(id, "id cannot be empty");
+		return this.idRegistrationMap.get(id);
+	}
+
+	@Nullable
+	@Override
+	public RegisteredClient findByClientId(String clientId) {
+		Assert.hasText(clientId, "clientId cannot be empty");
+		return this.clientIdRegistrationMap.get(clientId);
+	}
+
+	private void assertUniqueIdentifiers(RegisteredClient registeredClient,
+			Map<String, RegisteredClient> registrations) {
+		registrations.values().forEach((registration) -> {
+			if (registeredClient.getId().equals(registration.getId())) {
+				throw new IllegalArgumentException("Registered client must be unique. " + "Found duplicate identifier: "
+						+ registeredClient.getId());
+			}
+			if (registeredClient.getClientId().equals(registration.getClientId())) {
+				throw new IllegalArgumentException("Registered client must be unique. "
+						+ "Found duplicate client identifier: " + registeredClient.getClientId());
+			}
+			if (StringUtils.hasText(registeredClient.getClientSecret())
+					&& registeredClient.getClientSecret().equals(registration.getClientSecret())) {
+				throw new IllegalArgumentException("Registered client must be unique. "
+						+ "Found duplicate client secret for identifier: " + registeredClient.getId());
+			}
+		});
+	}
+
+}

+ 436 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepository.java

@@ -0,0 +1,436 @@
+/*
+ * Copyright 2020-2024 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.client;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.sql.Types;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.RuntimeHintsRegistrar;
+import org.springframework.context.annotation.ImportRuntimeHints;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
+import org.springframework.jdbc.core.JdbcOperations;
+import org.springframework.jdbc.core.PreparedStatementSetter;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.SqlParameterValue;
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
+import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
+import org.springframework.security.oauth2.server.authorization.settings.ConfigurationSettingNames;
+import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
+import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * A JDBC implementation of a {@link RegisteredClientRepository} that uses a
+ * {@link JdbcOperations} for {@link RegisteredClient} persistence.
+ *
+ * <p>
+ * <b>IMPORTANT:</b> This {@code RegisteredClientRepository} depends on the table
+ * definition described in
+ * "classpath:org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql"
+ * and therefore MUST be defined in the database schema.
+ *
+ * <p>
+ * <b>NOTE:</b> This {@code RegisteredClientRepository} is a simplified JDBC
+ * implementation that MAY be used in a production environment. However, it does have
+ * limitations as it likely won't perform well in an environment requiring high
+ * throughput. The expectation is that the consuming application will provide their own
+ * implementation of {@code RegisteredClientRepository} that meets the performance
+ * requirements for its deployment environment.
+ *
+ * @author Rafal Lewczuk
+ * @author Joe Grandja
+ * @author Ovidiu Popa
+ * @author Josh Long
+ * @since 0.1.2
+ * @see RegisteredClientRepository
+ * @see RegisteredClient
+ * @see JdbcOperations
+ * @see RowMapper
+ */
+@ImportRuntimeHints(JdbcRegisteredClientRepository.JdbcRegisteredClientRepositoryRuntimeHintsRegistrar.class)
+public class JdbcRegisteredClientRepository implements RegisteredClientRepository {
+
+	// @formatter:off
+	private static final String COLUMN_NAMES = "id, "
+			+ "client_id, "
+			+ "client_id_issued_at, "
+			+ "client_secret, "
+			+ "client_secret_expires_at, "
+			+ "client_name, "
+			+ "client_authentication_methods, "
+			+ "authorization_grant_types, "
+			+ "redirect_uris, "
+			+ "post_logout_redirect_uris, "
+			+ "scopes, "
+			+ "client_settings,"
+			+ "token_settings";
+	// @formatter:on
+
+	private static final String TABLE_NAME = "oauth2_registered_client";
+
+	private static final String PK_FILTER = "id = ?";
+
+	private static final String LOAD_REGISTERED_CLIENT_SQL = "SELECT " + COLUMN_NAMES + " FROM " + TABLE_NAME
+			+ " WHERE ";
+
+	// @formatter:off
+	private static final String INSERT_REGISTERED_CLIENT_SQL = "INSERT INTO " + TABLE_NAME
+			+ "(" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+	// @formatter:on
+
+	// @formatter:off
+	private static final String UPDATE_REGISTERED_CLIENT_SQL = "UPDATE " + TABLE_NAME
+			+ " SET client_secret = ?, client_secret_expires_at = ?, client_name = ?, client_authentication_methods = ?,"
+			+ " authorization_grant_types = ?, redirect_uris = ?, post_logout_redirect_uris = ?, scopes = ?,"
+			+ " client_settings = ?, token_settings = ?"
+			+ " WHERE " + PK_FILTER;
+	// @formatter:on
+
+	private static final String COUNT_REGISTERED_CLIENT_SQL = "SELECT COUNT(*) FROM " + TABLE_NAME + " WHERE ";
+
+	private final JdbcOperations jdbcOperations;
+
+	private RowMapper<RegisteredClient> registeredClientRowMapper;
+
+	private Function<RegisteredClient, List<SqlParameterValue>> registeredClientParametersMapper;
+
+	/**
+	 * Constructs a {@code JdbcRegisteredClientRepository} using the provided parameters.
+	 * @param jdbcOperations the JDBC operations
+	 */
+	public JdbcRegisteredClientRepository(JdbcOperations jdbcOperations) {
+		Assert.notNull(jdbcOperations, "jdbcOperations cannot be null");
+		this.jdbcOperations = jdbcOperations;
+		this.registeredClientRowMapper = new RegisteredClientRowMapper();
+		this.registeredClientParametersMapper = new RegisteredClientParametersMapper();
+	}
+
+	@Override
+	public void save(RegisteredClient registeredClient) {
+		Assert.notNull(registeredClient, "registeredClient cannot be null");
+		RegisteredClient existingRegisteredClient = findBy(PK_FILTER, registeredClient.getId());
+		if (existingRegisteredClient != null) {
+			updateRegisteredClient(registeredClient);
+		}
+		else {
+			insertRegisteredClient(registeredClient);
+		}
+	}
+
+	private void updateRegisteredClient(RegisteredClient registeredClient) {
+		List<SqlParameterValue> parameters = new ArrayList<>(
+				this.registeredClientParametersMapper.apply(registeredClient));
+		SqlParameterValue id = parameters.remove(0);
+		parameters.remove(0); // remove client_id
+		parameters.remove(0); // remove client_id_issued_at
+		parameters.add(id);
+		PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
+		this.jdbcOperations.update(UPDATE_REGISTERED_CLIENT_SQL, pss);
+	}
+
+	private void insertRegisteredClient(RegisteredClient registeredClient) {
+		assertUniqueIdentifiers(registeredClient);
+		List<SqlParameterValue> parameters = this.registeredClientParametersMapper.apply(registeredClient);
+		PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
+		this.jdbcOperations.update(INSERT_REGISTERED_CLIENT_SQL, pss);
+	}
+
+	private void assertUniqueIdentifiers(RegisteredClient registeredClient) {
+		Integer count = this.jdbcOperations.queryForObject(COUNT_REGISTERED_CLIENT_SQL + "client_id = ?", Integer.class,
+				registeredClient.getClientId());
+		if (count != null && count > 0) {
+			throw new IllegalArgumentException("Registered client must be unique. "
+					+ "Found duplicate client identifier: " + registeredClient.getClientId());
+		}
+		if (StringUtils.hasText(registeredClient.getClientSecret())) {
+			count = this.jdbcOperations.queryForObject(COUNT_REGISTERED_CLIENT_SQL + "client_secret = ?", Integer.class,
+					registeredClient.getClientSecret());
+			if (count != null && count > 0) {
+				throw new IllegalArgumentException("Registered client must be unique. "
+						+ "Found duplicate client secret for identifier: " + registeredClient.getId());
+			}
+		}
+	}
+
+	@Override
+	public RegisteredClient findById(String id) {
+		Assert.hasText(id, "id cannot be empty");
+		return findBy("id = ?", id);
+	}
+
+	@Override
+	public RegisteredClient findByClientId(String clientId) {
+		Assert.hasText(clientId, "clientId cannot be empty");
+		return findBy("client_id = ?", clientId);
+	}
+
+	private RegisteredClient findBy(String filter, Object... args) {
+		List<RegisteredClient> result = this.jdbcOperations.query(LOAD_REGISTERED_CLIENT_SQL + filter,
+				this.registeredClientRowMapper, args);
+		return !result.isEmpty() ? result.get(0) : null;
+	}
+
+	/**
+	 * Sets the {@link RowMapper} used for mapping the current row in
+	 * {@code java.sql.ResultSet} to {@link RegisteredClient}. The default is
+	 * {@link RegisteredClientRowMapper}.
+	 * @param registeredClientRowMapper the {@link RowMapper} used for mapping the current
+	 * row in {@code ResultSet} to {@link RegisteredClient}
+	 */
+	public final void setRegisteredClientRowMapper(RowMapper<RegisteredClient> registeredClientRowMapper) {
+		Assert.notNull(registeredClientRowMapper, "registeredClientRowMapper cannot be null");
+		this.registeredClientRowMapper = registeredClientRowMapper;
+	}
+
+	/**
+	 * Sets the {@code Function} used for mapping {@link RegisteredClient} to a
+	 * {@code List} of {@link SqlParameterValue}. The default is
+	 * {@link RegisteredClientParametersMapper}.
+	 * @param registeredClientParametersMapper the {@code Function} used for mapping
+	 * {@link RegisteredClient} to a {@code List} of {@link SqlParameterValue}
+	 */
+	public final void setRegisteredClientParametersMapper(
+			Function<RegisteredClient, List<SqlParameterValue>> registeredClientParametersMapper) {
+		Assert.notNull(registeredClientParametersMapper, "registeredClientParametersMapper cannot be null");
+		this.registeredClientParametersMapper = registeredClientParametersMapper;
+	}
+
+	protected final JdbcOperations getJdbcOperations() {
+		return this.jdbcOperations;
+	}
+
+	protected final RowMapper<RegisteredClient> getRegisteredClientRowMapper() {
+		return this.registeredClientRowMapper;
+	}
+
+	protected final Function<RegisteredClient, List<SqlParameterValue>> getRegisteredClientParametersMapper() {
+		return this.registeredClientParametersMapper;
+	}
+
+	/**
+	 * The default {@link RowMapper} that maps the current row in
+	 * {@code java.sql.ResultSet} to {@link RegisteredClient}.
+	 */
+	public static class RegisteredClientRowMapper implements RowMapper<RegisteredClient> {
+
+		private ObjectMapper objectMapper = new ObjectMapper();
+
+		public RegisteredClientRowMapper() {
+			ClassLoader classLoader = JdbcRegisteredClientRepository.class.getClassLoader();
+			List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
+			this.objectMapper.registerModules(securityModules);
+			this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
+		}
+
+		@Override
+		public RegisteredClient mapRow(ResultSet rs, int rowNum) throws SQLException {
+			Timestamp clientIdIssuedAt = rs.getTimestamp("client_id_issued_at");
+			Timestamp clientSecretExpiresAt = rs.getTimestamp("client_secret_expires_at");
+			Set<String> clientAuthenticationMethods = StringUtils
+				.commaDelimitedListToSet(rs.getString("client_authentication_methods"));
+			Set<String> authorizationGrantTypes = StringUtils
+				.commaDelimitedListToSet(rs.getString("authorization_grant_types"));
+			Set<String> redirectUris = StringUtils.commaDelimitedListToSet(rs.getString("redirect_uris"));
+			Set<String> postLogoutRedirectUris = StringUtils
+				.commaDelimitedListToSet(rs.getString("post_logout_redirect_uris"));
+			Set<String> clientScopes = StringUtils.commaDelimitedListToSet(rs.getString("scopes"));
+
+			// @formatter:off
+			RegisteredClient.Builder builder = RegisteredClient.withId(rs.getString("id"))
+					.clientId(rs.getString("client_id"))
+					.clientIdIssuedAt((clientIdIssuedAt != null) ? clientIdIssuedAt.toInstant() : null)
+					.clientSecret(rs.getString("client_secret"))
+					.clientSecretExpiresAt((clientSecretExpiresAt != null) ? clientSecretExpiresAt.toInstant() : null)
+					.clientName(rs.getString("client_name"))
+					.clientAuthenticationMethods((authenticationMethods) ->
+							clientAuthenticationMethods.forEach((authenticationMethod) ->
+									authenticationMethods.add(resolveClientAuthenticationMethod(authenticationMethod))))
+					.authorizationGrantTypes((grantTypes) ->
+							authorizationGrantTypes.forEach((grantType) ->
+									grantTypes.add(resolveAuthorizationGrantType(grantType))))
+					.redirectUris((uris) -> uris.addAll(redirectUris))
+					.postLogoutRedirectUris((uris) -> uris.addAll(postLogoutRedirectUris))
+					.scopes((scopes) -> scopes.addAll(clientScopes));
+			// @formatter:on
+
+			Map<String, Object> clientSettingsMap = parseMap(rs.getString("client_settings"));
+			builder.clientSettings(ClientSettings.withSettings(clientSettingsMap).build());
+
+			Map<String, Object> tokenSettingsMap = parseMap(rs.getString("token_settings"));
+			TokenSettings.Builder tokenSettingsBuilder = TokenSettings.withSettings(tokenSettingsMap);
+			if (!tokenSettingsMap.containsKey(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT)) {
+				tokenSettingsBuilder.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED);
+			}
+			builder.tokenSettings(tokenSettingsBuilder.build());
+
+			return builder.build();
+		}
+
+		public final void setObjectMapper(ObjectMapper objectMapper) {
+			Assert.notNull(objectMapper, "objectMapper cannot be null");
+			this.objectMapper = objectMapper;
+		}
+
+		protected final ObjectMapper getObjectMapper() {
+			return this.objectMapper;
+		}
+
+		private Map<String, Object> parseMap(String data) {
+			try {
+				return this.objectMapper.readValue(data, new TypeReference<>() {
+				});
+			}
+			catch (Exception ex) {
+				throw new IllegalArgumentException(ex.getMessage(), ex);
+			}
+		}
+
+		private static AuthorizationGrantType resolveAuthorizationGrantType(String authorizationGrantType) {
+			if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(authorizationGrantType)) {
+				return AuthorizationGrantType.AUTHORIZATION_CODE;
+			}
+			else if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(authorizationGrantType)) {
+				return AuthorizationGrantType.CLIENT_CREDENTIALS;
+			}
+			else if (AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(authorizationGrantType)) {
+				return AuthorizationGrantType.REFRESH_TOKEN;
+			}
+			// Custom authorization grant type
+			return new AuthorizationGrantType(authorizationGrantType);
+		}
+
+		private static ClientAuthenticationMethod resolveClientAuthenticationMethod(String clientAuthenticationMethod) {
+			if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue().equals(clientAuthenticationMethod)) {
+				return ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
+			}
+			else if (ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue().equals(clientAuthenticationMethod)) {
+				return ClientAuthenticationMethod.CLIENT_SECRET_POST;
+			}
+			else if (ClientAuthenticationMethod.NONE.getValue().equals(clientAuthenticationMethod)) {
+				return ClientAuthenticationMethod.NONE;
+			}
+			// Custom client authentication method
+			return new ClientAuthenticationMethod(clientAuthenticationMethod);
+		}
+
+	}
+
+	/**
+	 * The default {@code Function} that maps {@link RegisteredClient} to a {@code List}
+	 * of {@link SqlParameterValue}.
+	 */
+	public static class RegisteredClientParametersMapper
+			implements Function<RegisteredClient, List<SqlParameterValue>> {
+
+		private ObjectMapper objectMapper = new ObjectMapper();
+
+		public RegisteredClientParametersMapper() {
+			ClassLoader classLoader = JdbcRegisteredClientRepository.class.getClassLoader();
+			List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
+			this.objectMapper.registerModules(securityModules);
+			this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
+		}
+
+		@Override
+		public List<SqlParameterValue> apply(RegisteredClient registeredClient) {
+			Timestamp clientIdIssuedAt = (registeredClient.getClientIdIssuedAt() != null)
+					? Timestamp.from(registeredClient.getClientIdIssuedAt()) : Timestamp.from(Instant.now());
+
+			Timestamp clientSecretExpiresAt = (registeredClient.getClientSecretExpiresAt() != null)
+					? Timestamp.from(registeredClient.getClientSecretExpiresAt()) : null;
+
+			List<String> clientAuthenticationMethods = new ArrayList<>(
+					registeredClient.getClientAuthenticationMethods().size());
+			registeredClient.getClientAuthenticationMethods()
+				.forEach((clientAuthenticationMethod) -> clientAuthenticationMethods
+					.add(clientAuthenticationMethod.getValue()));
+
+			List<String> authorizationGrantTypes = new ArrayList<>(
+					registeredClient.getAuthorizationGrantTypes().size());
+			registeredClient.getAuthorizationGrantTypes()
+				.forEach((authorizationGrantType) -> authorizationGrantTypes.add(authorizationGrantType.getValue()));
+
+			return Arrays.asList(new SqlParameterValue(Types.VARCHAR, registeredClient.getId()),
+					new SqlParameterValue(Types.VARCHAR, registeredClient.getClientId()),
+					new SqlParameterValue(Types.TIMESTAMP, clientIdIssuedAt),
+					new SqlParameterValue(Types.VARCHAR, registeredClient.getClientSecret()),
+					new SqlParameterValue(Types.TIMESTAMP, clientSecretExpiresAt),
+					new SqlParameterValue(Types.VARCHAR, registeredClient.getClientName()),
+					new SqlParameterValue(Types.VARCHAR,
+							StringUtils.collectionToCommaDelimitedString(clientAuthenticationMethods)),
+					new SqlParameterValue(Types.VARCHAR,
+							StringUtils.collectionToCommaDelimitedString(authorizationGrantTypes)),
+					new SqlParameterValue(Types.VARCHAR,
+							StringUtils.collectionToCommaDelimitedString(registeredClient.getRedirectUris())),
+					new SqlParameterValue(Types.VARCHAR,
+							StringUtils.collectionToCommaDelimitedString(registeredClient.getPostLogoutRedirectUris())),
+					new SqlParameterValue(Types.VARCHAR,
+							StringUtils.collectionToCommaDelimitedString(registeredClient.getScopes())),
+					new SqlParameterValue(Types.VARCHAR, writeMap(registeredClient.getClientSettings().getSettings())),
+					new SqlParameterValue(Types.VARCHAR, writeMap(registeredClient.getTokenSettings().getSettings())));
+		}
+
+		public final void setObjectMapper(ObjectMapper objectMapper) {
+			Assert.notNull(objectMapper, "objectMapper cannot be null");
+			this.objectMapper = objectMapper;
+		}
+
+		protected final ObjectMapper getObjectMapper() {
+			return this.objectMapper;
+		}
+
+		private String writeMap(Map<String, Object> data) {
+			try {
+				return this.objectMapper.writeValueAsString(data);
+			}
+			catch (Exception ex) {
+				throw new IllegalArgumentException(ex.getMessage(), ex);
+			}
+		}
+
+	}
+
+	static class JdbcRegisteredClientRepositoryRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
+
+		@Override
+		public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
+			hints.resources()
+				.registerResource(new ClassPathResource(
+						"org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql"));
+		}
+
+	}
+
+}

+ 638 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/RegisteredClient.java

@@ -0,0 +1,638 @@
+/*
+ * Copyright 2020-2025 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.client;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
+import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * A representation of a client registration with an OAuth 2.0 Authorization Server.
+ *
+ * @author Joe Grandja
+ * @author Anoop Garlapati
+ * @since 0.0.1
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-2">Section 2
+ * Client Registration</a>
+ */
+public class RegisteredClient implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = -717282636175335081L;
+
+	private String id;
+
+	private String clientId;
+
+	private Instant clientIdIssuedAt;
+
+	private String clientSecret;
+
+	private Instant clientSecretExpiresAt;
+
+	private String clientName;
+
+	private Set<ClientAuthenticationMethod> clientAuthenticationMethods;
+
+	private Set<AuthorizationGrantType> authorizationGrantTypes;
+
+	private Set<String> redirectUris;
+
+	private Set<String> postLogoutRedirectUris;
+
+	private Set<String> scopes;
+
+	private ClientSettings clientSettings;
+
+	private TokenSettings tokenSettings;
+
+	protected RegisteredClient() {
+	}
+
+	/**
+	 * Returns the identifier for the registration.
+	 * @return the identifier for the registration
+	 */
+	public String getId() {
+		return this.id;
+	}
+
+	/**
+	 * Returns the client identifier.
+	 * @return the client identifier
+	 */
+	public String getClientId() {
+		return this.clientId;
+	}
+
+	/**
+	 * Returns the time at which the client identifier was issued.
+	 * @return the time at which the client identifier was issued
+	 */
+	@Nullable
+	public Instant getClientIdIssuedAt() {
+		return this.clientIdIssuedAt;
+	}
+
+	/**
+	 * Returns the client secret or {@code null} if not available.
+	 * @return the client secret or {@code null} if not available
+	 */
+	@Nullable
+	public String getClientSecret() {
+		return this.clientSecret;
+	}
+
+	/**
+	 * Returns the time at which the client secret expires or {@code null} if it does not
+	 * expire.
+	 * @return the time at which the client secret expires or {@code null} if it does not
+	 * expire
+	 */
+	@Nullable
+	public Instant getClientSecretExpiresAt() {
+		return this.clientSecretExpiresAt;
+	}
+
+	/**
+	 * Returns the client name.
+	 * @return the client name
+	 */
+	public String getClientName() {
+		return this.clientName;
+	}
+
+	/**
+	 * Returns the {@link ClientAuthenticationMethod authentication method(s)} that the
+	 * client may use.
+	 * @return the {@code Set} of {@link ClientAuthenticationMethod authentication
+	 * method(s)}
+	 */
+	public Set<ClientAuthenticationMethod> getClientAuthenticationMethods() {
+		return this.clientAuthenticationMethods;
+	}
+
+	/**
+	 * Returns the {@link AuthorizationGrantType authorization grant type(s)} that the
+	 * client may use.
+	 * @return the {@code Set} of {@link AuthorizationGrantType authorization grant
+	 * type(s)}
+	 */
+	public Set<AuthorizationGrantType> getAuthorizationGrantTypes() {
+		return this.authorizationGrantTypes;
+	}
+
+	/**
+	 * Returns the redirect URI(s) that the client may use in redirect-based flows.
+	 * @return the {@code Set} of redirect URI(s)
+	 */
+	public Set<String> getRedirectUris() {
+		return this.redirectUris;
+	}
+
+	/**
+	 * Returns the post logout redirect URI(s) that the client may use for logout. The
+	 * {@code post_logout_redirect_uri} parameter is used by the client when requesting
+	 * that the End-User's User Agent be redirected to after a logout has been performed.
+	 * @return the {@code Set} of post logout redirect URI(s)
+	 * @since 1.1
+	 */
+	public Set<String> getPostLogoutRedirectUris() {
+		return this.postLogoutRedirectUris;
+	}
+
+	/**
+	 * Returns the scope(s) that the client may use.
+	 * @return the {@code Set} of scope(s)
+	 */
+	public Set<String> getScopes() {
+		return this.scopes;
+	}
+
+	/**
+	 * Returns the {@link ClientSettings client configuration settings}.
+	 * @return the {@link ClientSettings}
+	 */
+	public ClientSettings getClientSettings() {
+		return this.clientSettings;
+	}
+
+	/**
+	 * Returns the {@link TokenSettings token configuration settings}.
+	 * @return the {@link TokenSettings}
+	 */
+	public TokenSettings getTokenSettings() {
+		return this.tokenSettings;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (obj == null || getClass() != obj.getClass()) {
+			return false;
+		}
+		RegisteredClient that = (RegisteredClient) obj;
+		return Objects.equals(this.id, that.id) && Objects.equals(this.clientId, that.clientId)
+				&& Objects.equals(this.clientIdIssuedAt, that.clientIdIssuedAt)
+				&& Objects.equals(this.clientSecret, that.clientSecret)
+				&& Objects.equals(this.clientSecretExpiresAt, that.clientSecretExpiresAt)
+				&& Objects.equals(this.clientName, that.clientName)
+				&& Objects.equals(this.clientAuthenticationMethods, that.clientAuthenticationMethods)
+				&& Objects.equals(this.authorizationGrantTypes, that.authorizationGrantTypes)
+				&& Objects.equals(this.redirectUris, that.redirectUris)
+				&& Objects.equals(this.postLogoutRedirectUris, that.postLogoutRedirectUris)
+				&& Objects.equals(this.scopes, that.scopes) && Objects.equals(this.clientSettings, that.clientSettings)
+				&& Objects.equals(this.tokenSettings, that.tokenSettings);
+	}
+
+	@Override
+	public int hashCode() {
+		return Objects.hash(this.id, this.clientId, this.clientIdIssuedAt, this.clientSecret,
+				this.clientSecretExpiresAt, this.clientName, this.clientAuthenticationMethods,
+				this.authorizationGrantTypes, this.redirectUris, this.postLogoutRedirectUris, this.scopes,
+				this.clientSettings, this.tokenSettings);
+	}
+
+	@Override
+	public String toString() {
+		return "RegisteredClient {" + "id='" + this.id + '\'' + ", clientId='" + this.clientId + '\'' + ", clientName='"
+				+ this.clientName + '\'' + ", clientAuthenticationMethods=" + this.clientAuthenticationMethods
+				+ ", authorizationGrantTypes=" + this.authorizationGrantTypes + ", redirectUris=" + this.redirectUris
+				+ ", postLogoutRedirectUris=" + this.postLogoutRedirectUris + ", scopes=" + this.scopes
+				+ ", clientSettings=" + this.clientSettings + ", tokenSettings=" + this.tokenSettings + '}';
+	}
+
+	/**
+	 * Returns a new {@link Builder}, initialized with the provided registration
+	 * identifier.
+	 * @param id the identifier for the registration
+	 * @return the {@link Builder}
+	 */
+	public static Builder withId(String id) {
+		Assert.hasText(id, "id cannot be empty");
+		return new Builder(id);
+	}
+
+	/**
+	 * Returns a new {@link Builder}, initialized with the values from the provided
+	 * {@link RegisteredClient}.
+	 * @param registeredClient the {@link RegisteredClient} used for initializing the
+	 * {@link Builder}
+	 * @return the {@link Builder}
+	 */
+	public static Builder from(RegisteredClient registeredClient) {
+		Assert.notNull(registeredClient, "registeredClient cannot be null");
+		return new Builder(registeredClient);
+	}
+
+	/**
+	 * A builder for {@link RegisteredClient}.
+	 */
+	public static class Builder {
+
+		private String id;
+
+		private String clientId;
+
+		private Instant clientIdIssuedAt;
+
+		private String clientSecret;
+
+		private Instant clientSecretExpiresAt;
+
+		private String clientName;
+
+		private final Set<ClientAuthenticationMethod> clientAuthenticationMethods = new HashSet<>();
+
+		private final Set<AuthorizationGrantType> authorizationGrantTypes = new HashSet<>();
+
+		private final Set<String> redirectUris = new HashSet<>();
+
+		private final Set<String> postLogoutRedirectUris = new HashSet<>();
+
+		private final Set<String> scopes = new HashSet<>();
+
+		private ClientSettings clientSettings;
+
+		private TokenSettings tokenSettings;
+
+		protected Builder(String id) {
+			this.id = id;
+		}
+
+		protected Builder(RegisteredClient registeredClient) {
+			this.id = registeredClient.getId();
+			this.clientId = registeredClient.getClientId();
+			this.clientIdIssuedAt = registeredClient.getClientIdIssuedAt();
+			this.clientSecret = registeredClient.getClientSecret();
+			this.clientSecretExpiresAt = registeredClient.getClientSecretExpiresAt();
+			this.clientName = registeredClient.getClientName();
+			if (!CollectionUtils.isEmpty(registeredClient.getClientAuthenticationMethods())) {
+				this.clientAuthenticationMethods.addAll(registeredClient.getClientAuthenticationMethods());
+			}
+			if (!CollectionUtils.isEmpty(registeredClient.getAuthorizationGrantTypes())) {
+				this.authorizationGrantTypes.addAll(registeredClient.getAuthorizationGrantTypes());
+			}
+			if (!CollectionUtils.isEmpty(registeredClient.getRedirectUris())) {
+				this.redirectUris.addAll(registeredClient.getRedirectUris());
+			}
+			if (!CollectionUtils.isEmpty(registeredClient.getPostLogoutRedirectUris())) {
+				this.postLogoutRedirectUris.addAll(registeredClient.getPostLogoutRedirectUris());
+			}
+			if (!CollectionUtils.isEmpty(registeredClient.getScopes())) {
+				this.scopes.addAll(registeredClient.getScopes());
+			}
+			this.clientSettings = ClientSettings.withSettings(registeredClient.getClientSettings().getSettings())
+				.build();
+			this.tokenSettings = TokenSettings.withSettings(registeredClient.getTokenSettings().getSettings()).build();
+		}
+
+		/**
+		 * Sets the identifier for the registration.
+		 * @param id the identifier for the registration
+		 * @return the {@link Builder}
+		 */
+		public Builder id(String id) {
+			this.id = id;
+			return this;
+		}
+
+		/**
+		 * Sets the client identifier.
+		 * @param clientId the client identifier
+		 * @return the {@link Builder}
+		 */
+		public Builder clientId(String clientId) {
+			this.clientId = clientId;
+			return this;
+		}
+
+		/**
+		 * Sets the time at which the client identifier was issued.
+		 * @param clientIdIssuedAt the time at which the client identifier was issued
+		 * @return the {@link Builder}
+		 */
+		public Builder clientIdIssuedAt(Instant clientIdIssuedAt) {
+			this.clientIdIssuedAt = clientIdIssuedAt;
+			return this;
+		}
+
+		/**
+		 * Sets the client secret.
+		 * @param clientSecret the client secret
+		 * @return the {@link Builder}
+		 */
+		public Builder clientSecret(String clientSecret) {
+			this.clientSecret = clientSecret;
+			return this;
+		}
+
+		/**
+		 * Sets the time at which the client secret expires or {@code null} if it does not
+		 * expire.
+		 * @param clientSecretExpiresAt the time at which the client secret expires or
+		 * {@code null} if it does not expire
+		 * @return the {@link Builder}
+		 */
+		public Builder clientSecretExpiresAt(Instant clientSecretExpiresAt) {
+			this.clientSecretExpiresAt = clientSecretExpiresAt;
+			return this;
+		}
+
+		/**
+		 * Sets the client name.
+		 * @param clientName the client name
+		 * @return the {@link Builder}
+		 */
+		public Builder clientName(String clientName) {
+			this.clientName = clientName;
+			return this;
+		}
+
+		/**
+		 * Adds an {@link ClientAuthenticationMethod authentication method} the client may
+		 * use when authenticating with the authorization server.
+		 * @param clientAuthenticationMethod the authentication method
+		 * @return the {@link Builder}
+		 */
+		public Builder clientAuthenticationMethod(ClientAuthenticationMethod clientAuthenticationMethod) {
+			this.clientAuthenticationMethods.add(clientAuthenticationMethod);
+			return this;
+		}
+
+		/**
+		 * A {@code Consumer} of the {@link ClientAuthenticationMethod authentication
+		 * method(s)} allowing the ability to add, replace, or remove.
+		 * @param clientAuthenticationMethodsConsumer a {@code Consumer} of the
+		 * authentication method(s)
+		 * @return the {@link Builder}
+		 */
+		public Builder clientAuthenticationMethods(
+				Consumer<Set<ClientAuthenticationMethod>> clientAuthenticationMethodsConsumer) {
+			clientAuthenticationMethodsConsumer.accept(this.clientAuthenticationMethods);
+			return this;
+		}
+
+		/**
+		 * Adds an {@link AuthorizationGrantType authorization grant type} the client may
+		 * use.
+		 * @param authorizationGrantType the authorization grant type
+		 * @return the {@link Builder}
+		 */
+		public Builder authorizationGrantType(AuthorizationGrantType authorizationGrantType) {
+			this.authorizationGrantTypes.add(authorizationGrantType);
+			return this;
+		}
+
+		/**
+		 * A {@code Consumer} of the {@link AuthorizationGrantType authorization grant
+		 * type(s)} allowing the ability to add, replace, or remove.
+		 * @param authorizationGrantTypesConsumer a {@code Consumer} of the authorization
+		 * grant type(s)
+		 * @return the {@link Builder}
+		 */
+		public Builder authorizationGrantTypes(Consumer<Set<AuthorizationGrantType>> authorizationGrantTypesConsumer) {
+			authorizationGrantTypesConsumer.accept(this.authorizationGrantTypes);
+			return this;
+		}
+
+		/**
+		 * Adds a redirect URI the client may use in a redirect-based flow.
+		 * @param redirectUri the redirect URI
+		 * @return the {@link Builder}
+		 */
+		public Builder redirectUri(String redirectUri) {
+			this.redirectUris.add(redirectUri);
+			return this;
+		}
+
+		/**
+		 * A {@code Consumer} of the redirect URI(s) allowing the ability to add, replace,
+		 * or remove.
+		 * @param redirectUrisConsumer a {@link Consumer} of the redirect URI(s)
+		 * @return the {@link Builder}
+		 */
+		public Builder redirectUris(Consumer<Set<String>> redirectUrisConsumer) {
+			redirectUrisConsumer.accept(this.redirectUris);
+			return this;
+		}
+
+		/**
+		 * Adds a post logout redirect URI the client may use for logout. The
+		 * {@code post_logout_redirect_uri} parameter is used by the client when
+		 * requesting that the End-User's User Agent be redirected to after a logout has
+		 * been performed.
+		 * @param postLogoutRedirectUri the post logout redirect URI
+		 * @return the {@link Builder}
+		 * @since 1.1
+		 */
+		public Builder postLogoutRedirectUri(String postLogoutRedirectUri) {
+			this.postLogoutRedirectUris.add(postLogoutRedirectUri);
+			return this;
+		}
+
+		/**
+		 * A {@code Consumer} of the post logout redirect URI(s) allowing the ability to
+		 * add, replace, or remove.
+		 * @param postLogoutRedirectUrisConsumer a {@link Consumer} of the post logout
+		 * redirect URI(s)
+		 * @return the {@link Builder}
+		 * @since 1.1
+		 */
+		public Builder postLogoutRedirectUris(Consumer<Set<String>> postLogoutRedirectUrisConsumer) {
+			postLogoutRedirectUrisConsumer.accept(this.postLogoutRedirectUris);
+			return this;
+		}
+
+		/**
+		 * Adds a scope the client may use.
+		 * @param scope the scope
+		 * @return the {@link Builder}
+		 */
+		public Builder scope(String scope) {
+			this.scopes.add(scope);
+			return this;
+		}
+
+		/**
+		 * A {@code Consumer} of the scope(s) allowing the ability to add, replace, or
+		 * remove.
+		 * @param scopesConsumer a {@link Consumer} of the scope(s)
+		 * @return the {@link Builder}
+		 */
+		public Builder scopes(Consumer<Set<String>> scopesConsumer) {
+			scopesConsumer.accept(this.scopes);
+			return this;
+		}
+
+		/**
+		 * Sets the {@link ClientSettings client configuration settings}.
+		 * @param clientSettings the client configuration settings
+		 * @return the {@link Builder}
+		 */
+		public Builder clientSettings(ClientSettings clientSettings) {
+			this.clientSettings = clientSettings;
+			return this;
+		}
+
+		/**
+		 * Sets the {@link TokenSettings token configuration settings}.
+		 * @param tokenSettings the token configuration settings
+		 * @return the {@link Builder}
+		 */
+		public Builder tokenSettings(TokenSettings tokenSettings) {
+			this.tokenSettings = tokenSettings;
+			return this;
+		}
+
+		/**
+		 * Builds a new {@link RegisteredClient}.
+		 * @return a {@link RegisteredClient}
+		 */
+		public RegisteredClient build() {
+			Assert.hasText(this.clientId, "clientId cannot be empty");
+			Assert.notEmpty(this.authorizationGrantTypes, "authorizationGrantTypes cannot be empty");
+			if (this.authorizationGrantTypes.contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
+				Assert.notEmpty(this.redirectUris, "redirectUris cannot be empty");
+			}
+			if (!StringUtils.hasText(this.clientName)) {
+				this.clientName = this.id;
+			}
+			if (CollectionUtils.isEmpty(this.clientAuthenticationMethods)) {
+				this.clientAuthenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+			}
+			if (this.clientSettings == null) {
+				ClientSettings.Builder builder = ClientSettings.builder();
+				if (isPublicClientType()) {
+					// @formatter:off
+					builder
+							.requireProofKey(true)
+							.requireAuthorizationConsent(true);
+					// @formatter:on
+				}
+				this.clientSettings = builder.build();
+			}
+			if (this.tokenSettings == null) {
+				this.tokenSettings = TokenSettings.builder().build();
+			}
+			validateScopes();
+			validateRedirectUris();
+			validatePostLogoutRedirectUris();
+			return create();
+		}
+
+		private boolean isPublicClientType() {
+			return this.authorizationGrantTypes.contains(AuthorizationGrantType.AUTHORIZATION_CODE)
+					&& this.clientAuthenticationMethods.size() == 1
+					&& this.clientAuthenticationMethods.contains(ClientAuthenticationMethod.NONE);
+		}
+
+		private RegisteredClient create() {
+			RegisteredClient registeredClient = new RegisteredClient();
+
+			registeredClient.id = this.id;
+			registeredClient.clientId = this.clientId;
+			registeredClient.clientIdIssuedAt = this.clientIdIssuedAt;
+			registeredClient.clientSecret = this.clientSecret;
+			registeredClient.clientSecretExpiresAt = this.clientSecretExpiresAt;
+			registeredClient.clientName = this.clientName;
+			registeredClient.clientAuthenticationMethods = Collections
+				.unmodifiableSet(new HashSet<>(this.clientAuthenticationMethods));
+			registeredClient.authorizationGrantTypes = Collections
+				.unmodifiableSet(new HashSet<>(this.authorizationGrantTypes));
+			registeredClient.redirectUris = Collections.unmodifiableSet(new HashSet<>(this.redirectUris));
+			registeredClient.postLogoutRedirectUris = Collections
+				.unmodifiableSet(new HashSet<>(this.postLogoutRedirectUris));
+			registeredClient.scopes = Collections.unmodifiableSet(new HashSet<>(this.scopes));
+			registeredClient.clientSettings = this.clientSettings;
+			registeredClient.tokenSettings = this.tokenSettings;
+
+			return registeredClient;
+		}
+
+		private void validateScopes() {
+			if (CollectionUtils.isEmpty(this.scopes)) {
+				return;
+			}
+
+			for (String scope : this.scopes) {
+				Assert.isTrue(validateScope(scope), "scope \"" + scope + "\" contains invalid characters");
+			}
+		}
+
+		private static boolean validateScope(String scope) {
+			return scope == null || scope.chars()
+				.allMatch((c) -> withinTheRangeOf(c, 0x21, 0x21) || withinTheRangeOf(c, 0x23, 0x5B)
+						|| withinTheRangeOf(c, 0x5D, 0x7E));
+		}
+
+		private static boolean withinTheRangeOf(int c, int min, int max) {
+			return c >= min && c <= max;
+		}
+
+		private void validateRedirectUris() {
+			if (CollectionUtils.isEmpty(this.redirectUris)) {
+				return;
+			}
+
+			for (String redirectUri : this.redirectUris) {
+				Assert.isTrue(validateRedirectUri(redirectUri),
+						"redirect_uri \"" + redirectUri + "\" is not a valid redirect URI or contains fragment");
+			}
+		}
+
+		private void validatePostLogoutRedirectUris() {
+			if (CollectionUtils.isEmpty(this.postLogoutRedirectUris)) {
+				return;
+			}
+
+			for (String postLogoutRedirectUri : this.postLogoutRedirectUris) {
+				Assert.isTrue(validateRedirectUri(postLogoutRedirectUri), "post_logout_redirect_uri \""
+						+ postLogoutRedirectUri + "\" is not a valid post logout redirect URI or contains fragment");
+			}
+		}
+
+		private static boolean validateRedirectUri(String redirectUri) {
+			try {
+				URI validRedirectUri = new URI(redirectUri);
+				return validRedirectUri.getFragment() == null;
+			}
+			catch (URISyntaxException ex) {
+				return false;
+			}
+		}
+
+	}
+
+}

+ 59 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/RegisteredClientRepository.java

@@ -0,0 +1,59 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization.client;
+
+import org.springframework.lang.Nullable;
+
+/**
+ * A repository for OAuth 2.0 {@link RegisteredClient}(s).
+ *
+ * @author Joe Grandja
+ * @author Anoop Garlapati
+ * @author Ovidiu Popa
+ * @since 0.0.1
+ * @see RegisteredClient
+ */
+public interface RegisteredClientRepository {
+
+	/**
+	 * Saves the registered client.
+	 *
+	 * <p>
+	 * IMPORTANT: Sensitive information should be encoded externally from the
+	 * implementation, e.g. {@link RegisteredClient#getClientSecret()}
+	 * @param registeredClient the {@link RegisteredClient}
+	 */
+	void save(RegisteredClient registeredClient);
+
+	/**
+	 * Returns the registered client identified by the provided {@code id}, or
+	 * {@code null} if not found.
+	 * @param id the registration identifier
+	 * @return the {@link RegisteredClient} if found, otherwise {@code null}
+	 */
+	@Nullable
+	RegisteredClient findById(String id);
+
+	/**
+	 * Returns the registered client identified by the provided {@code clientId}, or
+	 * {@code null} if not found.
+	 * @param clientId the client identifier
+	 * @return the {@link RegisteredClient} if found, otherwise {@code null}
+	 */
+	@Nullable
+	RegisteredClient findByClientId(String clientId);
+
+}

+ 90 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configuration/OAuth2AuthorizationServerConfiguration.java

@@ -0,0 +1,90 @@
+/*
+ * Copyright 2020-2025 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.configuration;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.source.JWKSource;
+import com.nimbusds.jose.proc.JWSKeySelector;
+import com.nimbusds.jose.proc.JWSVerificationKeySelector;
+import com.nimbusds.jose.proc.SecurityContext;
+import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
+import com.nimbusds.jwt.proc.DefaultJWTProcessor;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
+import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.security.web.SecurityFilterChain;
+
+/**
+ * {@link Configuration} for OAuth 2.0 Authorization Server support.
+ *
+ * @author Joe Grandja
+ * @since 0.0.1
+ * @see OAuth2AuthorizationServerConfigurer
+ */
+@Configuration(proxyBeanMethods = false)
+public class OAuth2AuthorizationServerConfiguration {
+
+	@Bean
+	@Order(Ordered.HIGHEST_PRECEDENCE)
+	public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
+		// @formatter:off
+		OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
+				OAuth2AuthorizationServerConfigurer.authorizationServer();
+		http
+			.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
+			.with(authorizationServerConfigurer, Customizer.withDefaults())
+			.authorizeHttpRequests((authorize) ->
+				authorize.anyRequest().authenticated()
+			);
+		// @formatter:on
+		return http.build();
+	}
+
+	public static JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
+		Set<JWSAlgorithm> jwsAlgs = new HashSet<>();
+		jwsAlgs.addAll(JWSAlgorithm.Family.RSA);
+		jwsAlgs.addAll(JWSAlgorithm.Family.EC);
+		jwsAlgs.addAll(JWSAlgorithm.Family.HMAC_SHA);
+		ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
+		JWSKeySelector<SecurityContext> jwsKeySelector = new JWSVerificationKeySelector<>(jwsAlgs, jwkSource);
+		jwtProcessor.setJWSKeySelector(jwsKeySelector);
+		// Override the default Nimbus claims set verifier as NimbusJwtDecoder handles it
+		// instead
+		jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
+		});
+		return new NimbusJwtDecoder(jwtProcessor);
+	}
+
+	@Bean
+	RegisterMissingBeanPostProcessor registerMissingBeanPostProcessor() {
+		RegisterMissingBeanPostProcessor postProcessor = new RegisterMissingBeanPostProcessor();
+		postProcessor.addBeanDefinition(AuthorizationServerSettings.class,
+				() -> AuthorizationServerSettings.builder().build());
+		return postProcessor;
+	}
+
+}

+ 75 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configuration/RegisterMissingBeanPostProcessor.java

@@ -0,0 +1,75 @@
+/*
+ * 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.configuration;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.beans.factory.BeanFactoryUtils;
+import org.springframework.beans.factory.ListableBeanFactory;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.support.AbstractBeanDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
+import org.springframework.beans.factory.support.RootBeanDefinition;
+import org.springframework.context.annotation.AnnotationBeanNameGenerator;
+
+/**
+ * Post processor to register one or more bean definitions on container initialization, if
+ * not already present.
+ *
+ * @author Steve Riesenberg
+ * @since 0.2.0
+ */
+final class RegisterMissingBeanPostProcessor implements BeanDefinitionRegistryPostProcessor, BeanFactoryAware {
+
+	private final AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
+
+	private final List<AbstractBeanDefinition> beanDefinitions = new ArrayList<>();
+
+	private BeanFactory beanFactory;
+
+	@Override
+	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
+		for (AbstractBeanDefinition beanDefinition : this.beanDefinitions) {
+			String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
+					(ListableBeanFactory) this.beanFactory, beanDefinition.getBeanClass(), false, false);
+			if (beanNames.length == 0) {
+				String beanName = this.beanNameGenerator.generateBeanName(beanDefinition, registry);
+				registry.registerBeanDefinition(beanName, beanDefinition);
+			}
+		}
+	}
+
+	@Override
+	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
+	}
+
+	<T> void addBeanDefinition(Class<T> beanClass, Supplier<T> beanSupplier) {
+		this.beanDefinitions.add(new RootBeanDefinition(beanClass, beanSupplier));
+	}
+
+	@Override
+	public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
+		this.beanFactory = beanFactory;
+	}
+
+}

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

@@ -0,0 +1,50 @@
+/*
+ * Copyright 2020-2025 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 org.springframework.security.config.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+
+/**
+ * Base configurer for an OAuth 2.0 component (e.g. protocol endpoint).
+ *
+ * @author Joe Grandja
+ * @since 0.1.2
+ */
+abstract class AbstractOAuth2Configurer {
+
+	private final ObjectPostProcessor<Object> objectPostProcessor;
+
+	AbstractOAuth2Configurer(ObjectPostProcessor<Object> objectPostProcessor) {
+		this.objectPostProcessor = objectPostProcessor;
+	}
+
+	abstract void init(HttpSecurity httpSecurity);
+
+	abstract void configure(HttpSecurity httpSecurity);
+
+	abstract RequestMatcher getRequestMatcher();
+
+	protected final <T> T postProcess(T object) {
+		return (T) this.objectPostProcessor.postProcess(object);
+	}
+
+	protected final ObjectPostProcessor<Object> getObjectPostProcessor() {
+		return this.objectPostProcessor;
+	}
+
+}

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

@@ -0,0 +1,155 @@
+/*
+ * Copyright 2020-2025 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.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.security.web.util.UrlUtils;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+import org.springframework.web.util.UriComponentsBuilder;
+
+/**
+ * A {@code Filter} that associates the {@link AuthorizationServerContext} to the
+ * {@link AuthorizationServerContextHolder}.
+ *
+ * @author Joe Grandja
+ * @since 0.2.2
+ * @see AuthorizationServerContext
+ * @see AuthorizationServerContextHolder
+ * @see AuthorizationServerSettings
+ */
+final class AuthorizationServerContextFilter extends OncePerRequestFilter {
+
+	private final AuthorizationServerSettings authorizationServerSettings;
+
+	private final IssuerResolver issuerResolver;
+
+	AuthorizationServerContextFilter(AuthorizationServerSettings authorizationServerSettings) {
+		Assert.notNull(authorizationServerSettings, "authorizationServerSettings cannot be null");
+		this.authorizationServerSettings = authorizationServerSettings;
+		this.issuerResolver = new IssuerResolver(authorizationServerSettings);
+	}
+
+	@Override
+	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+			throws ServletException, IOException {
+
+		try {
+			String issuer = this.issuerResolver.resolve(request);
+			AuthorizationServerContext authorizationServerContext = new DefaultAuthorizationServerContext(issuer,
+					this.authorizationServerSettings);
+			AuthorizationServerContextHolder.setContext(authorizationServerContext);
+			filterChain.doFilter(request, response);
+		}
+		finally {
+			AuthorizationServerContextHolder.resetContext();
+		}
+	}
+
+	private static final class IssuerResolver {
+
+		private final String issuer;
+
+		private final Set<String> endpointUris;
+
+		private IssuerResolver(AuthorizationServerSettings authorizationServerSettings) {
+			if (authorizationServerSettings.getIssuer() != null) {
+				this.issuer = authorizationServerSettings.getIssuer();
+				this.endpointUris = Collections.emptySet();
+			}
+			else {
+				this.issuer = null;
+				this.endpointUris = new HashSet<>();
+				this.endpointUris.add("/.well-known/oauth-authorization-server");
+				this.endpointUris.add("/.well-known/openid-configuration");
+				for (Map.Entry<String, Object> setting : authorizationServerSettings.getSettings().entrySet()) {
+					if (setting.getKey().endsWith("-endpoint")) {
+						this.endpointUris.add((String) setting.getValue());
+					}
+				}
+			}
+		}
+
+		private String resolve(HttpServletRequest request) {
+			if (this.issuer != null) {
+				return this.issuer;
+			}
+
+			// Resolve Issuer Identifier dynamically from request
+			String path = request.getRequestURI();
+			if (!StringUtils.hasText(path)) {
+				path = "";
+			}
+			else {
+				for (String endpointUri : this.endpointUris) {
+					if (path.contains(endpointUri)) {
+						path = path.replace(endpointUri, "");
+						break;
+					}
+				}
+			}
+
+			// @formatter:off
+			return UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request))
+					.replacePath(path)
+					.replaceQuery(null)
+					.fragment(null)
+					.build()
+					.toUriString();
+			// @formatter:on
+		}
+
+	}
+
+	private static final class DefaultAuthorizationServerContext implements AuthorizationServerContext {
+
+		private final String issuer;
+
+		private final AuthorizationServerSettings authorizationServerSettings;
+
+		private DefaultAuthorizationServerContext(String issuer,
+				AuthorizationServerSettings authorizationServerSettings) {
+			this.issuer = issuer;
+			this.authorizationServerSettings = authorizationServerSettings;
+		}
+
+		@Override
+		public String getIssuer() {
+			return this.issuer;
+		}
+
+		@Override
+		public AuthorizationServerSettings getAuthorizationServerSettings() {
+			return this.authorizationServerSettings;
+		}
+
+	}
+
+}

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

@@ -0,0 +1,147 @@
+/*
+ * Copyright 2020-2025 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.security.MessageDigest;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import com.nimbusds.jose.jwk.JWK;
+
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+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.jwt.Jwt;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimNames;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * @author Joe Grandja
+ * @author Steve Riesenberg
+ * @since 1.3
+ */
+final class DefaultOAuth2TokenCustomizers {
+
+	private DefaultOAuth2TokenCustomizers() {
+	}
+
+	static OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
+		return (context) -> context.getClaims().claims((claims) -> customize(context, claims));
+	}
+
+	static OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer() {
+		return (context) -> context.getClaims().claims((claims) -> customize(context, claims));
+	}
+
+	private static void customize(OAuth2TokenContext tokenContext, Map<String, Object> claims) {
+		Map<String, Object> cnfClaims = null;
+
+		// Add 'cnf' claim for Mutual-TLS Client Certificate-Bound Access Tokens
+		if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenContext.getTokenType())
+				&& tokenContext.getAuthorizationGrant() != null && tokenContext.getAuthorizationGrant()
+					.getPrincipal() instanceof OAuth2ClientAuthenticationToken clientAuthentication) {
+
+			if ((ClientAuthenticationMethod.TLS_CLIENT_AUTH.equals(clientAuthentication.getClientAuthenticationMethod())
+					|| ClientAuthenticationMethod.SELF_SIGNED_TLS_CLIENT_AUTH
+						.equals(clientAuthentication.getClientAuthenticationMethod()))
+					&& tokenContext.getRegisteredClient().getTokenSettings().isX509CertificateBoundAccessTokens()) {
+
+				X509Certificate[] clientCertificateChain = (X509Certificate[]) clientAuthentication.getCredentials();
+				try {
+					String sha256Thumbprint = computeSHA256Thumbprint(clientCertificateChain[0]);
+					cnfClaims = new HashMap<>();
+					cnfClaims.put("x5t#S256", sha256Thumbprint);
+				}
+				catch (Exception ex) {
+					OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
+							"Failed to compute SHA-256 Thumbprint for client X509Certificate.", null);
+					throw new OAuth2AuthenticationException(error, ex);
+				}
+			}
+		}
+
+		// Add 'cnf' claim for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
+		Jwt dPoPProofJwt = tokenContext.get(OAuth2TokenContext.DPOP_PROOF_KEY);
+		if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenContext.getTokenType()) && dPoPProofJwt != null) {
+			JWK jwk = null;
+			@SuppressWarnings("unchecked")
+			Map<String, Object> jwkJson = (Map<String, Object>) dPoPProofJwt.getHeaders().get("jwk");
+			try {
+				jwk = JWK.parse(jwkJson);
+			}
+			catch (Exception ignored) {
+			}
+			if (jwk == null) {
+				OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF,
+						"jwk header is missing or invalid.", null);
+				throw new OAuth2AuthenticationException(error);
+			}
+
+			try {
+				String sha256Thumbprint = jwk.computeThumbprint().toString();
+				if (cnfClaims == null) {
+					cnfClaims = new HashMap<>();
+				}
+				cnfClaims.put("jkt", sha256Thumbprint);
+			}
+			catch (Exception ex) {
+				OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
+						"Failed to compute SHA-256 Thumbprint for DPoP Proof PublicKey.", null);
+				throw new OAuth2AuthenticationException(error, ex);
+			}
+		}
+
+		if (!CollectionUtils.isEmpty(cnfClaims)) {
+			claims.put("cnf", cnfClaims);
+		}
+
+		// Add 'act' claim for delegation use case of Token Exchange Grant.
+		// If more than one actor is present, we create a chain of delegation by nesting
+		// "act" claims.
+		if (tokenContext
+			.getPrincipal() instanceof OAuth2TokenExchangeCompositeAuthenticationToken compositeAuthenticationToken) {
+			Map<String, Object> currentClaims = claims;
+			for (OAuth2TokenExchangeActor actor : compositeAuthenticationToken.getActors()) {
+				Map<String, Object> actorClaims = actor.getClaims();
+				Map<String, Object> actClaim = new LinkedHashMap<>();
+				actClaim.put(OAuth2TokenClaimNames.ISS, actorClaims.get(OAuth2TokenClaimNames.ISS));
+				actClaim.put(OAuth2TokenClaimNames.SUB, actorClaims.get(OAuth2TokenClaimNames.SUB));
+				currentClaims.put("act", Collections.unmodifiableMap(actClaim));
+				currentClaims = actClaim;
+			}
+		}
+	}
+
+	private static String computeSHA256Thumbprint(X509Certificate x509Certificate) throws Exception {
+		MessageDigest md = MessageDigest.getInstance("SHA-256");
+		byte[] digest = md.digest(x509Certificate.getEncoded());
+		return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
+	}
+
+}

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

@@ -0,0 +1,327 @@
+/*
+ * Copyright 2020-2025 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.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationContext;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationValidator;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
+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.OAuth2AuthorizationCodeRequestAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationConsentAuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
+import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
+import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
+import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
+import org.springframework.security.web.util.matcher.OrRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Configurer for the OAuth 2.0 Authorization Endpoint.
+ *
+ * @author Joe Grandja
+ * @since 0.1.2
+ * @see OAuth2AuthorizationServerConfigurer#authorizationEndpoint
+ * @see OAuth2AuthorizationEndpointFilter
+ */
+public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2Configurer {
+
+	private RequestMatcher requestMatcher;
+
+	private 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;
+
+	private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authorizationCodeRequestAuthenticationValidator;
+
+	private SessionAuthenticationStrategy sessionAuthenticationStrategy;
+
+	/**
+	 * Restrict for internal use only.
+	 * @param objectPostProcessor an {@code ObjectPostProcessor}
+	 */
+	OAuth2AuthorizationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
+		super(objectPostProcessor);
+	}
+
+	/**
+	 * Adds an {@link AuthenticationConverter} used when attempting to extract an
+	 * Authorization Request (or Consent) from {@link HttpServletRequest} to an instance
+	 * of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} or
+	 * {@link OAuth2AuthorizationConsentAuthenticationToken} used for authenticating the
+	 * request.
+	 * @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) {
+		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;
+	}
+
+	/**
+	 * Adds an {@link AuthenticationProvider} used for authenticating an
+	 * {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
+	 * @param authenticationProvider an {@link AuthenticationProvider} used for
+	 * authenticating an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
+	 * @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
+	 */
+	public OAuth2AuthorizationEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
+		Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
+		this.authenticationProviders.add(authenticationProvider);
+		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}.
+	 * @param authorizationResponseHandler the {@link AuthenticationSuccessHandler} used
+	 * for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
+	 * @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
+	 */
+	public OAuth2AuthorizationEndpointConfigurer authorizationResponseHandler(
+			AuthenticationSuccessHandler authorizationResponseHandler) {
+		this.authorizationResponseHandler = authorizationResponseHandler;
+		return this;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationFailureHandler} used for handling an
+	 * {@link OAuth2AuthorizationCodeRequestAuthenticationException} and returning the
+	 * {@link OAuth2Error Error Response}.
+	 * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
+	 * handling an {@link OAuth2AuthorizationCodeRequestAuthenticationException}
+	 * @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
+	 */
+	public OAuth2AuthorizationEndpointConfigurer errorResponseHandler(
+			AuthenticationFailureHandler errorResponseHandler) {
+		this.errorResponseHandler = errorResponseHandler;
+		return this;
+	}
+
+	/**
+	 * Specify the URI to redirect Resource Owners to if consent is required during the
+	 * {@code authorization_code} flow. A default consent page will be generated when this
+	 * attribute is not specified.
+	 *
+	 * If a URI is specified, applications are required to process the specified URI to
+	 * generate a consent page. The query string will contain the following parameters:
+	 *
+	 * <ul>
+	 * <li>{@code client_id} - the client identifier</li>
+	 * <li>{@code scope} - a space-delimited list of scopes present in the authorization
+	 * request</li>
+	 * <li>{@code state} - a CSRF protection token</li>
+	 * </ul>
+	 *
+	 * In general, the consent page should create a form that submits a request with the
+	 * following requirements:
+	 *
+	 * <ul>
+	 * <li>It must be an HTTP POST</li>
+	 * <li>It must be submitted to
+	 * {@link AuthorizationServerSettings#getAuthorizationEndpoint()}</li>
+	 * <li>It must include the received {@code client_id} as an HTTP parameter</li>
+	 * <li>It must include the received {@code state} as an HTTP parameter</li>
+	 * <li>It must include the list of {@code scope}s the {@code Resource Owner} consented
+	 * to as an HTTP parameter</li>
+	 * </ul>
+	 * @param consentPage the URI of the custom consent page to redirect to if consent is
+	 * required (e.g. "/oauth2/consent")
+	 * @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
+	 */
+	public OAuth2AuthorizationEndpointConfigurer consentPage(String consentPage) {
+		this.consentPage = consentPage;
+		return this;
+	}
+
+	void addAuthorizationCodeRequestAuthenticationValidator(
+			Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) {
+		this.authorizationCodeRequestAuthenticationValidator = (this.authorizationCodeRequestAuthenticationValidator == null)
+				? authenticationValidator
+				: this.authorizationCodeRequestAuthenticationValidator.andThen(authenticationValidator);
+	}
+
+	void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthenticationStrategy) {
+		this.sessionAuthenticationStrategy = sessionAuthenticationStrategy;
+	}
+
+	@Override
+	void init(HttpSecurity httpSecurity) {
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+		String authorizationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getAuthorizationEndpoint())
+				: authorizationServerSettings.getAuthorizationEndpoint();
+		this.requestMatcher = new OrRequestMatcher(
+				PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, authorizationEndpointUri),
+				PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, authorizationEndpointUri));
+		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)));
+	}
+
+	@Override
+	void configure(HttpSecurity httpSecurity) {
+		AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+		String authorizationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getAuthorizationEndpoint())
+				: authorizationServerSettings.getAuthorizationEndpoint();
+		OAuth2AuthorizationEndpointFilter authorizationEndpointFilter = new OAuth2AuthorizationEndpointFilter(
+				authenticationManager, authorizationEndpointUri);
+		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);
+		}
+		if (this.errorResponseHandler != null) {
+			authorizationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
+		}
+		if (StringUtils.hasText(this.consentPage)) {
+			authorizationEndpointFilter.setConsentPage(this.consentPage);
+		}
+		if (this.sessionAuthenticationStrategy != null) {
+			authorizationEndpointFilter.setSessionAuthenticationStrategy(this.sessionAuthenticationStrategy);
+		}
+		httpSecurity.addFilterBefore(postProcess(authorizationEndpointFilter),
+				AbstractPreAuthenticatedProcessingFilter.class);
+	}
+
+	@Override
+	RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+	private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
+		List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
+
+		authenticationConverters.add(new OAuth2AuthorizationCodeRequestAuthenticationConverter());
+		authenticationConverters.add(new OAuth2AuthorizationConsentAuthenticationConverter());
+
+		return authenticationConverters;
+	}
+
+	private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
+		List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+
+		OAuth2AuthorizationCodeRequestAuthenticationProvider authorizationCodeRequestAuthenticationProvider = new OAuth2AuthorizationCodeRequestAuthenticationProvider(
+				OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
+				OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity),
+				OAuth2ConfigurerUtils.getAuthorizationConsentService(httpSecurity));
+		if (this.authorizationCodeRequestAuthenticationValidator != null) {
+			authorizationCodeRequestAuthenticationProvider
+				.setAuthenticationValidator(new OAuth2AuthorizationCodeRequestAuthenticationValidator()
+					.andThen(this.authorizationCodeRequestAuthenticationValidator));
+		}
+		authenticationProviders.add(authorizationCodeRequestAuthenticationProvider);
+
+		OAuth2AuthorizationConsentAuthenticationProvider authorizationConsentAuthenticationProvider = new OAuth2AuthorizationConsentAuthenticationProvider(
+				OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
+				OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity),
+				OAuth2ConfigurerUtils.getAuthorizationConsentService(httpSecurity));
+		authenticationProviders.add(authorizationConsentAuthenticationProvider);
+
+		return authenticationProviders;
+	}
+
+}

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

@@ -0,0 +1,506 @@
+/*
+ * Copyright 2020-2025 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.net.URI;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import com.nimbusds.jose.jwk.source.JWKSource;
+
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.GenericApplicationListenerAdapter;
+import org.springframework.context.event.SmartApplicationListener;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
+import org.springframework.security.context.DelegatingApplicationListener;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.session.SessionRegistry;
+import org.springframework.security.core.session.SessionRegistryImpl;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.OAuth2Token;
+import org.springframework.security.oauth2.core.oidc.OidcScopes;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationContext;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
+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.token.OAuth2TokenGenerator;
+import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter;
+import org.springframework.security.web.authentication.HttpStatusEntryPoint;
+import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
+import org.springframework.security.web.context.SecurityContextHolderFilter;
+import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
+import org.springframework.security.web.util.matcher.OrRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link AbstractHttpConfigurer} for OAuth 2.0 Authorization Server support.
+ *
+ * @author Joe Grandja
+ * @author Daniel Garnier-Moiroux
+ * @author Gerardo Roza
+ * @author Ovidiu Popa
+ * @author Gaurav Tiwari
+ * @since 0.0.1
+ * @see AbstractHttpConfigurer
+ * @see OAuth2ClientAuthenticationConfigurer
+ * @see OAuth2AuthorizationServerMetadataEndpointConfigurer
+ * @see OAuth2AuthorizationEndpointConfigurer
+ * @see OAuth2PushedAuthorizationRequestEndpointConfigurer
+ * @see OAuth2TokenEndpointConfigurer
+ * @see OAuth2TokenIntrospectionEndpointConfigurer
+ * @see OAuth2TokenRevocationEndpointConfigurer
+ * @see OAuth2DeviceAuthorizationEndpointConfigurer
+ * @see OAuth2DeviceVerificationEndpointConfigurer
+ * @see OidcConfigurer
+ * @see RegisteredClientRepository
+ * @see OAuth2AuthorizationService
+ * @see OAuth2AuthorizationConsentService
+ * @see NimbusJwkSetEndpointFilter
+ */
+public final class OAuth2AuthorizationServerConfigurer
+		extends AbstractHttpConfigurer<OAuth2AuthorizationServerConfigurer, HttpSecurity> {
+
+	private final Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = createConfigurers();
+
+	private RequestMatcher endpointsMatcher;
+
+	/**
+	 * Returns a new instance of {@link OAuth2AuthorizationServerConfigurer} for
+	 * configuring.
+	 * @return a new instance of {@link OAuth2AuthorizationServerConfigurer} for
+	 * configuring
+	 * @since 1.4
+	 */
+	public static OAuth2AuthorizationServerConfigurer authorizationServer() {
+		return new OAuth2AuthorizationServerConfigurer();
+	}
+
+	/**
+	 * Sets the repository of registered clients.
+	 * @param registeredClientRepository the repository of registered clients
+	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
+	 */
+	public OAuth2AuthorizationServerConfigurer registeredClientRepository(
+			RegisteredClientRepository registeredClientRepository) {
+		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+		getBuilder().setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
+		return this;
+	}
+
+	/**
+	 * Sets the authorization service.
+	 * @param authorizationService the authorization service
+	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
+	 */
+	public OAuth2AuthorizationServerConfigurer authorizationService(OAuth2AuthorizationService authorizationService) {
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		getBuilder().setSharedObject(OAuth2AuthorizationService.class, authorizationService);
+		return this;
+	}
+
+	/**
+	 * Sets the authorization consent service.
+	 * @param authorizationConsentService the authorization consent service
+	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
+	 */
+	public OAuth2AuthorizationServerConfigurer authorizationConsentService(
+			OAuth2AuthorizationConsentService authorizationConsentService) {
+		Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
+		getBuilder().setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
+		return this;
+	}
+
+	/**
+	 * Sets the authorization server settings.
+	 * @param authorizationServerSettings the authorization server settings
+	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
+	 */
+	public OAuth2AuthorizationServerConfigurer authorizationServerSettings(
+			AuthorizationServerSettings authorizationServerSettings) {
+		Assert.notNull(authorizationServerSettings, "authorizationServerSettings cannot be null");
+		getBuilder().setSharedObject(AuthorizationServerSettings.class, authorizationServerSettings);
+		return this;
+	}
+
+	/**
+	 * Sets the token generator.
+	 * @param tokenGenerator the token generator
+	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
+	 * @since 0.2.3
+	 */
+	public OAuth2AuthorizationServerConfigurer tokenGenerator(
+			OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
+		Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
+		getBuilder().setSharedObject(OAuth2TokenGenerator.class, tokenGenerator);
+		return this;
+	}
+
+	/**
+	 * Configures OAuth 2.0 Client Authentication.
+	 * @param clientAuthenticationCustomizer the {@link Customizer} providing access to
+	 * the {@link OAuth2ClientAuthenticationConfigurer}
+	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
+	 */
+	public OAuth2AuthorizationServerConfigurer clientAuthentication(
+			Customizer<OAuth2ClientAuthenticationConfigurer> clientAuthenticationCustomizer) {
+		clientAuthenticationCustomizer.customize(getConfigurer(OAuth2ClientAuthenticationConfigurer.class));
+		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.
+	 * @param authorizationEndpointCustomizer the {@link Customizer} providing access to
+	 * the {@link OAuth2AuthorizationEndpointConfigurer}
+	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
+	 */
+	public OAuth2AuthorizationServerConfigurer authorizationEndpoint(
+			Customizer<OAuth2AuthorizationEndpointConfigurer> authorizationEndpointCustomizer) {
+		authorizationEndpointCustomizer.customize(getConfigurer(OAuth2AuthorizationEndpointConfigurer.class));
+		return this;
+	}
+
+	/**
+	 * Configures the OAuth 2.0 Pushed Authorization Request Endpoint.
+	 * @param pushedAuthorizationRequestEndpointCustomizer the {@link Customizer}
+	 * providing access to the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer}
+	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
+	 * @since 1.5
+	 */
+	public OAuth2AuthorizationServerConfigurer pushedAuthorizationRequestEndpoint(
+			Customizer<OAuth2PushedAuthorizationRequestEndpointConfigurer> pushedAuthorizationRequestEndpointCustomizer) {
+		OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestEndpointConfigurer = getConfigurer(
+				OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
+		if (pushedAuthorizationRequestEndpointConfigurer == null) {
+			addConfigurer(OAuth2PushedAuthorizationRequestEndpointConfigurer.class,
+					new OAuth2PushedAuthorizationRequestEndpointConfigurer(this::postProcess));
+			pushedAuthorizationRequestEndpointConfigurer = getConfigurer(
+					OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
+		}
+		pushedAuthorizationRequestEndpointCustomizer.customize(pushedAuthorizationRequestEndpointConfigurer);
+		return this;
+	}
+
+	/**
+	 * Configures the OAuth 2.0 Token Endpoint.
+	 * @param tokenEndpointCustomizer the {@link Customizer} providing access to the
+	 * {@link OAuth2TokenEndpointConfigurer}
+	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
+	 */
+	public OAuth2AuthorizationServerConfigurer tokenEndpoint(
+			Customizer<OAuth2TokenEndpointConfigurer> tokenEndpointCustomizer) {
+		tokenEndpointCustomizer.customize(getConfigurer(OAuth2TokenEndpointConfigurer.class));
+		return this;
+	}
+
+	/**
+	 * Configures the OAuth 2.0 Token Introspection Endpoint.
+	 * @param tokenIntrospectionEndpointCustomizer the {@link Customizer} providing access
+	 * to the {@link OAuth2TokenIntrospectionEndpointConfigurer}
+	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
+	 * @since 0.2.3
+	 */
+	public OAuth2AuthorizationServerConfigurer tokenIntrospectionEndpoint(
+			Customizer<OAuth2TokenIntrospectionEndpointConfigurer> tokenIntrospectionEndpointCustomizer) {
+		tokenIntrospectionEndpointCustomizer.customize(getConfigurer(OAuth2TokenIntrospectionEndpointConfigurer.class));
+		return this;
+	}
+
+	/**
+	 * Configures the OAuth 2.0 Token Revocation Endpoint.
+	 * @param tokenRevocationEndpointCustomizer the {@link Customizer} providing access to
+	 * the {@link OAuth2TokenRevocationEndpointConfigurer}
+	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
+	 * @since 0.2.2
+	 */
+	public OAuth2AuthorizationServerConfigurer tokenRevocationEndpoint(
+			Customizer<OAuth2TokenRevocationEndpointConfigurer> tokenRevocationEndpointCustomizer) {
+		tokenRevocationEndpointCustomizer.customize(getConfigurer(OAuth2TokenRevocationEndpointConfigurer.class));
+		return this;
+	}
+
+	/**
+	 * Configures the OAuth 2.0 Device Authorization Endpoint.
+	 * @param deviceAuthorizationEndpointCustomizer the {@link Customizer} providing
+	 * access to the {@link OAuth2DeviceAuthorizationEndpointConfigurer}
+	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
+	 * @since 1.1
+	 */
+	public OAuth2AuthorizationServerConfigurer deviceAuthorizationEndpoint(
+			Customizer<OAuth2DeviceAuthorizationEndpointConfigurer> deviceAuthorizationEndpointCustomizer) {
+		deviceAuthorizationEndpointCustomizer
+			.customize(getConfigurer(OAuth2DeviceAuthorizationEndpointConfigurer.class));
+		return this;
+	}
+
+	/**
+	 * Configures the OAuth 2.0 Device Verification Endpoint.
+	 * @param deviceVerificationEndpointCustomizer the {@link Customizer} providing access
+	 * to the {@link OAuth2DeviceVerificationEndpointConfigurer}
+	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
+	 * @since 1.1
+	 */
+	public OAuth2AuthorizationServerConfigurer deviceVerificationEndpoint(
+			Customizer<OAuth2DeviceVerificationEndpointConfigurer> deviceVerificationEndpointCustomizer) {
+		deviceVerificationEndpointCustomizer.customize(getConfigurer(OAuth2DeviceVerificationEndpointConfigurer.class));
+		return this;
+	}
+
+	/**
+	 * Configures OpenID Connect 1.0 support (disabled by default).
+	 * @param oidcCustomizer the {@link Customizer} providing access to the
+	 * {@link OidcConfigurer}
+	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
+	 */
+	public OAuth2AuthorizationServerConfigurer oidc(Customizer<OidcConfigurer> oidcCustomizer) {
+		OidcConfigurer oidcConfigurer = getConfigurer(OidcConfigurer.class);
+		if (oidcConfigurer == null) {
+			addConfigurer(OidcConfigurer.class, new OidcConfigurer(this::postProcess));
+			oidcConfigurer = getConfigurer(OidcConfigurer.class);
+		}
+		oidcCustomizer.customize(oidcConfigurer);
+		return this;
+	}
+
+	/**
+	 * Returns a {@link RequestMatcher} for the authorization server endpoints.
+	 * @return a {@link RequestMatcher} for the authorization server endpoints
+	 */
+	public RequestMatcher getEndpointsMatcher() {
+		// Return a deferred RequestMatcher
+		// since endpointsMatcher is constructed in init(HttpSecurity).
+		return (request) -> this.endpointsMatcher.matches(request);
+	}
+
+	@Override
+	public void init(HttpSecurity httpSecurity) throws Exception {
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+		validateAuthorizationServerSettings(authorizationServerSettings);
+
+		if (isOidcEnabled()) {
+			// Add OpenID Connect session tracking capabilities.
+			initSessionRegistry(httpSecurity);
+			SessionRegistry sessionRegistry = httpSecurity.getSharedObject(SessionRegistry.class);
+			OAuth2AuthorizationEndpointConfigurer authorizationEndpointConfigurer = getConfigurer(
+					OAuth2AuthorizationEndpointConfigurer.class);
+			authorizationEndpointConfigurer.setSessionAuthenticationStrategy((authentication, request, response) -> {
+				if (authentication instanceof OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
+					if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
+						if (sessionRegistry.getSessionInformation(request.getSession().getId()) == null) {
+							sessionRegistry.registerNewSession(request.getSession().getId(),
+									((Authentication) authorizationCodeRequestAuthentication.getPrincipal())
+										.getPrincipal());
+						}
+					}
+				}
+			});
+		}
+		else {
+			// OpenID Connect is disabled.
+			// Add an authentication validator that rejects authentication requests.
+			Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> oidcAuthenticationRequestValidator = (
+					authenticationContext) -> {
+				OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationContext
+					.getAuthentication();
+				if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
+					OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE,
+							"OpenID Connect 1.0 authentication requests are restricted.",
+							"https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1");
+					throw new OAuth2AuthorizationCodeRequestAuthenticationException(error,
+							authorizationCodeRequestAuthentication);
+				}
+			};
+			OAuth2AuthorizationEndpointConfigurer authorizationEndpointConfigurer = getConfigurer(
+					OAuth2AuthorizationEndpointConfigurer.class);
+			authorizationEndpointConfigurer
+				.addAuthorizationCodeRequestAuthenticationValidator(oidcAuthenticationRequestValidator);
+			OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestEndpointConfigurer = getConfigurer(
+					OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
+			if (pushedAuthorizationRequestEndpointConfigurer != null) {
+				pushedAuthorizationRequestEndpointConfigurer
+					.addAuthorizationCodeRequestAuthenticationValidator(oidcAuthenticationRequestValidator);
+			}
+		}
+
+		List<RequestMatcher> requestMatchers = new ArrayList<>();
+		this.configurers.values().forEach((configurer) -> {
+			configurer.init(httpSecurity);
+			requestMatchers.add(configurer.getRequestMatcher());
+		});
+		String jwkSetEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils.withMultipleIssuersPattern(authorizationServerSettings.getJwkSetEndpoint())
+				: authorizationServerSettings.getJwkSetEndpoint();
+		requestMatchers.add(PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, jwkSetEndpointUri));
+		this.endpointsMatcher = new OrRequestMatcher(requestMatchers);
+
+		ExceptionHandlingConfigurer<HttpSecurity> exceptionHandling = httpSecurity
+			.getConfigurer(ExceptionHandlingConfigurer.class);
+		if (exceptionHandling != null) {
+			List<RequestMatcher> preferredMatchers = new ArrayList<>();
+			preferredMatchers.add(getRequestMatcher(OAuth2TokenEndpointConfigurer.class));
+			preferredMatchers.add(getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class));
+			preferredMatchers.add(getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class));
+			preferredMatchers.add(getRequestMatcher(OAuth2DeviceAuthorizationEndpointConfigurer.class));
+			RequestMatcher preferredMatcher = getRequestMatcher(
+					OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
+			if (preferredMatcher != null) {
+				preferredMatchers.add(preferredMatcher);
+			}
+			exceptionHandling.defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
+					new OrRequestMatcher(preferredMatchers));
+		}
+
+		httpSecurity.csrf((csrf) -> csrf.ignoringRequestMatchers(this.endpointsMatcher));
+
+		OidcConfigurer oidcConfigurer = getConfigurer(OidcConfigurer.class);
+		if (oidcConfigurer != null) {
+			if (oidcConfigurer.getConfigurer(OidcUserInfoEndpointConfigurer.class) != null
+					|| oidcConfigurer.getConfigurer(OidcClientRegistrationEndpointConfigurer.class) != null) {
+				httpSecurity
+					// Accept access tokens for User Info and/or Client Registration
+					.oauth2ResourceServer(
+							(oauth2ResourceServer) -> oauth2ResourceServer.jwt(Customizer.withDefaults()));
+
+			}
+		}
+	}
+
+	@Override
+	public void configure(HttpSecurity httpSecurity) {
+		this.configurers.values().forEach((configurer) -> configurer.configure(httpSecurity));
+
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+
+		AuthorizationServerContextFilter authorizationServerContextFilter = new AuthorizationServerContextFilter(
+				authorizationServerSettings);
+		httpSecurity.addFilterAfter(postProcess(authorizationServerContextFilter), SecurityContextHolderFilter.class);
+
+		JWKSource<com.nimbusds.jose.proc.SecurityContext> jwkSource = OAuth2ConfigurerUtils.getJwkSource(httpSecurity);
+		if (jwkSource != null) {
+			String jwkSetEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+					? OAuth2ConfigurerUtils.withMultipleIssuersPattern(authorizationServerSettings.getJwkSetEndpoint())
+					: authorizationServerSettings.getJwkSetEndpoint();
+			NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter(jwkSource,
+					jwkSetEndpointUri);
+			httpSecurity.addFilterBefore(postProcess(jwkSetEndpointFilter),
+					AbstractPreAuthenticatedProcessingFilter.class);
+		}
+	}
+
+	private boolean isOidcEnabled() {
+		return getConfigurer(OidcConfigurer.class) != null;
+	}
+
+	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));
+		configurers.put(OAuth2TokenRevocationEndpointConfigurer.class,
+				new OAuth2TokenRevocationEndpointConfigurer(this::postProcess));
+		configurers.put(OAuth2DeviceAuthorizationEndpointConfigurer.class,
+				new OAuth2DeviceAuthorizationEndpointConfigurer(this::postProcess));
+		configurers.put(OAuth2DeviceVerificationEndpointConfigurer.class,
+				new OAuth2DeviceVerificationEndpointConfigurer(this::postProcess));
+		return configurers;
+	}
+
+	@SuppressWarnings("unchecked")
+	private <T> T getConfigurer(Class<T> type) {
+		return (T) this.configurers.get(type);
+	}
+
+	private <T extends AbstractOAuth2Configurer> void addConfigurer(Class<T> configurerType, T configurer) {
+		this.configurers.put(configurerType, configurer);
+	}
+
+	private <T extends AbstractOAuth2Configurer> RequestMatcher getRequestMatcher(Class<T> configurerType) {
+		T configurer = getConfigurer(configurerType);
+		return (configurer != null) ? configurer.getRequestMatcher() : null;
+	}
+
+	private static void validateAuthorizationServerSettings(AuthorizationServerSettings authorizationServerSettings) {
+		if (authorizationServerSettings.getIssuer() != null) {
+			URI issuerUri;
+			try {
+				issuerUri = new URI(authorizationServerSettings.getIssuer());
+				issuerUri.toURL();
+			}
+			catch (Exception ex) {
+				throw new IllegalArgumentException("issuer must be a valid URL", ex);
+			}
+			// rfc8414 https://datatracker.ietf.org/doc/html/rfc8414#section-2
+			if (issuerUri.getQuery() != null || issuerUri.getFragment() != null) {
+				throw new IllegalArgumentException("issuer cannot contain query or fragment component");
+			}
+		}
+	}
+
+	private static void initSessionRegistry(HttpSecurity httpSecurity) {
+		SessionRegistry sessionRegistry = OAuth2ConfigurerUtils.getOptionalBean(httpSecurity, SessionRegistry.class);
+		if (sessionRegistry == null) {
+			sessionRegistry = new SessionRegistryImpl();
+			registerDelegateApplicationListener(httpSecurity, (SessionRegistryImpl) sessionRegistry);
+		}
+		httpSecurity.setSharedObject(SessionRegistry.class, sessionRegistry);
+	}
+
+	private static void registerDelegateApplicationListener(HttpSecurity httpSecurity,
+			ApplicationListener<?> delegate) {
+		DelegatingApplicationListener delegatingApplicationListener = OAuth2ConfigurerUtils
+			.getOptionalBean(httpSecurity, DelegatingApplicationListener.class);
+		if (delegatingApplicationListener == null) {
+			return;
+		}
+		SmartApplicationListener smartListener = new GenericApplicationListenerAdapter(delegate);
+		delegatingApplicationListener.addListener(smartListener);
+	}
+
+}

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

@@ -0,0 +1,119 @@
+/*
+ * Copyright 2020-2025 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.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.settings.AuthorizationServerSettings;
+import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter;
+import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
+import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
+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.
+	 * @param objectPostProcessor an {@code ObjectPostProcessor}
+	 */
+	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) {
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+		String authorizationServerMetadataEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? "/.well-known/oauth-authorization-server/**" : "/.well-known/oauth-authorization-server";
+		this.requestMatcher = PathPatternRequestMatcher.withDefaults()
+			.matcher(HttpMethod.GET, authorizationServerMetadataEndpointUri);
+	}
+
+	@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)
+						? authorizationServerMetadataCustomizer.andThen(this.authorizationServerMetadataCustomizer)
+						: this.authorizationServerMetadataCustomizer;
+			}
+		}
+		return authorizationServerMetadataCustomizer;
+	}
+
+	@Override
+	RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+}

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

@@ -0,0 +1,288 @@
+/*
+ * Copyright 2020-2025 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.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.server.authorization.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.OAuth2ClientAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.PublicClientAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.X509ClientCertificateAuthenticationProvider;
+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.JwtClientAssertionAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.PublicClientAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.X509ClientCertificateAuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
+import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
+import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
+import org.springframework.security.web.util.matcher.OrRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+
+/**
+ * Configurer for OAuth 2.0 Client Authentication.
+ *
+ * @author Joe Grandja
+ * @since 0.2.0
+ * @see OAuth2AuthorizationServerConfigurer#clientAuthentication
+ * @see OAuth2ClientAuthenticationFilter
+ */
+public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Configurer {
+
+	private RequestMatcher requestMatcher;
+
+	private 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;
+
+	/**
+	 * Restrict for internal use only.
+	 * @param objectPostProcessor an {@code ObjectPostProcessor}
+	 */
+	OAuth2ClientAuthenticationConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
+		super(objectPostProcessor);
+	}
+
+	/**
+	 * 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 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) {
+		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;
+	}
+
+	/**
+	 * Adds an {@link AuthenticationProvider} used for authenticating an
+	 * {@link OAuth2ClientAuthenticationToken}.
+	 * @param authenticationProvider an {@link AuthenticationProvider} used for
+	 * authenticating an {@link OAuth2ClientAuthenticationToken}
+	 * @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
+	 */
+	public OAuth2ClientAuthenticationConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
+		Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
+		this.authenticationProviders.add(authenticationProvider);
+		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}.
+	 * @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used
+	 * for handling a successful client authentication
+	 * @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
+	 */
+	public OAuth2ClientAuthenticationConfigurer authenticationSuccessHandler(
+			AuthenticationSuccessHandler authenticationSuccessHandler) {
+		this.authenticationSuccessHandler = authenticationSuccessHandler;
+		return this;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationFailureHandler} used for handling a failed client
+	 * authentication and returning the {@link OAuth2Error Error Response}.
+	 * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
+	 * handling a failed client authentication
+	 * @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
+	 */
+	public OAuth2ClientAuthenticationConfigurer errorResponseHandler(
+			AuthenticationFailureHandler errorResponseHandler) {
+		this.errorResponseHandler = errorResponseHandler;
+		return this;
+	}
+
+	@Override
+	void init(HttpSecurity httpSecurity) {
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+		String tokenEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils.withMultipleIssuersPattern(authorizationServerSettings.getTokenEndpoint())
+				: authorizationServerSettings.getTokenEndpoint();
+		String tokenIntrospectionEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getTokenIntrospectionEndpoint())
+				: authorizationServerSettings.getTokenIntrospectionEndpoint();
+		String tokenRevocationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getTokenRevocationEndpoint())
+				: authorizationServerSettings.getTokenRevocationEndpoint();
+		String deviceAuthorizationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint())
+				: authorizationServerSettings.getDeviceAuthorizationEndpoint();
+		String pushedAuthorizationRequestEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getPushedAuthorizationRequestEndpoint())
+				: authorizationServerSettings.getPushedAuthorizationRequestEndpoint();
+		this.requestMatcher = new OrRequestMatcher(
+				PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, tokenEndpointUri),
+				PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, tokenIntrospectionEndpointUri),
+				PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, tokenRevocationEndpointUri),
+				PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, deviceAuthorizationEndpointUri),
+				PathPatternRequestMatcher.withDefaults()
+					.matcher(HttpMethod.POST, pushedAuthorizationRequestEndpointUri));
+		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)));
+	}
+
+	@Override
+	void configure(HttpSecurity httpSecurity) {
+		AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
+		OAuth2ClientAuthenticationFilter clientAuthenticationFilter = new OAuth2ClientAuthenticationFilter(
+				authenticationManager, this.requestMatcher);
+		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);
+		}
+		if (this.errorResponseHandler != null) {
+			clientAuthenticationFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
+		}
+		httpSecurity.addFilterAfter(postProcess(clientAuthenticationFilter),
+				AbstractPreAuthenticatedProcessingFilter.class);
+	}
+
+	@Override
+	RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+	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());
+		authenticationConverters.add(new X509ClientCertificateAuthenticationConverter());
+
+		return authenticationConverters;
+	}
+
+	private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
+		List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+
+		RegisteredClientRepository registeredClientRepository = OAuth2ConfigurerUtils
+			.getRegisteredClientRepository(httpSecurity);
+		OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity);
+
+		JwtClientAssertionAuthenticationProvider jwtClientAssertionAuthenticationProvider = new JwtClientAssertionAuthenticationProvider(
+				registeredClientRepository, authorizationService);
+		authenticationProviders.add(jwtClientAssertionAuthenticationProvider);
+
+		X509ClientCertificateAuthenticationProvider x509ClientCertificateAuthenticationProvider = new X509ClientCertificateAuthenticationProvider(
+				registeredClientRepository, authorizationService);
+		authenticationProviders.add(x509ClientCertificateAuthenticationProvider);
+
+		ClientSecretAuthenticationProvider clientSecretAuthenticationProvider = new ClientSecretAuthenticationProvider(
+				registeredClientRepository, authorizationService);
+		PasswordEncoder passwordEncoder = OAuth2ConfigurerUtils.getOptionalBean(httpSecurity, PasswordEncoder.class);
+		if (passwordEncoder != null) {
+			clientSecretAuthenticationProvider.setPasswordEncoder(passwordEncoder);
+		}
+		authenticationProviders.add(clientSecretAuthenticationProvider);
+
+		PublicClientAuthenticationProvider publicClientAuthenticationProvider = new PublicClientAuthenticationProvider(
+				registeredClientRepository, authorizationService);
+		authenticationProviders.add(publicClientAuthenticationProvider);
+
+		return authenticationProviders;
+	}
+
+}

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

@@ -0,0 +1,246 @@
+/*
+ * Copyright 2020-2024 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.Map;
+
+import com.nimbusds.jose.jwk.source.JWKSource;
+import com.nimbusds.jose.proc.SecurityContext;
+
+import org.springframework.beans.factory.BeanFactoryUtils;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.ResolvableType;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.oauth2.core.OAuth2Token;
+import org.springframework.security.oauth2.jwt.JwtEncoder;
+import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
+import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationConsentService;
+import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+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.token.DelegatingOAuth2TokenGenerator;
+import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
+import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Utility methods for the OAuth 2.0 Configurers.
+ *
+ * @author Joe Grandja
+ * @since 0.1.2
+ */
+final class OAuth2ConfigurerUtils {
+
+	private OAuth2ConfigurerUtils() {
+	}
+
+	static String withMultipleIssuersPattern(String endpointUri) {
+		Assert.hasText(endpointUri, "endpointUri cannot be empty");
+		return endpointUri.startsWith("/") ? "/**" + endpointUri : "/**/" + endpointUri;
+	}
+
+	static RegisteredClientRepository getRegisteredClientRepository(HttpSecurity httpSecurity) {
+		RegisteredClientRepository registeredClientRepository = httpSecurity
+			.getSharedObject(RegisteredClientRepository.class);
+		if (registeredClientRepository == null) {
+			registeredClientRepository = getBean(httpSecurity, RegisteredClientRepository.class);
+			httpSecurity.setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
+		}
+		return registeredClientRepository;
+	}
+
+	static OAuth2AuthorizationService getAuthorizationService(HttpSecurity httpSecurity) {
+		OAuth2AuthorizationService authorizationService = httpSecurity
+			.getSharedObject(OAuth2AuthorizationService.class);
+		if (authorizationService == null) {
+			authorizationService = getOptionalBean(httpSecurity, OAuth2AuthorizationService.class);
+			if (authorizationService == null) {
+				authorizationService = new InMemoryOAuth2AuthorizationService();
+			}
+			httpSecurity.setSharedObject(OAuth2AuthorizationService.class, authorizationService);
+		}
+		return authorizationService;
+	}
+
+	static OAuth2AuthorizationConsentService getAuthorizationConsentService(HttpSecurity httpSecurity) {
+		OAuth2AuthorizationConsentService authorizationConsentService = httpSecurity
+			.getSharedObject(OAuth2AuthorizationConsentService.class);
+		if (authorizationConsentService == null) {
+			authorizationConsentService = getOptionalBean(httpSecurity, OAuth2AuthorizationConsentService.class);
+			if (authorizationConsentService == null) {
+				authorizationConsentService = new InMemoryOAuth2AuthorizationConsentService();
+			}
+			httpSecurity.setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
+		}
+		return authorizationConsentService;
+	}
+
+	@SuppressWarnings("unchecked")
+	static OAuth2TokenGenerator<? extends OAuth2Token> getTokenGenerator(HttpSecurity httpSecurity) {
+		OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator = httpSecurity
+			.getSharedObject(OAuth2TokenGenerator.class);
+		if (tokenGenerator == null) {
+			tokenGenerator = getOptionalBean(httpSecurity, OAuth2TokenGenerator.class);
+			if (tokenGenerator == null) {
+				JwtGenerator jwtGenerator = getJwtGenerator(httpSecurity);
+				OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
+				accessTokenGenerator.setAccessTokenCustomizer(getAccessTokenCustomizer(httpSecurity));
+				OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
+				if (jwtGenerator != null) {
+					tokenGenerator = new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator,
+							refreshTokenGenerator);
+				}
+				else {
+					tokenGenerator = new DelegatingOAuth2TokenGenerator(accessTokenGenerator, refreshTokenGenerator);
+				}
+			}
+			httpSecurity.setSharedObject(OAuth2TokenGenerator.class, tokenGenerator);
+		}
+		return tokenGenerator;
+	}
+
+	private static JwtGenerator getJwtGenerator(HttpSecurity httpSecurity) {
+		JwtGenerator jwtGenerator = httpSecurity.getSharedObject(JwtGenerator.class);
+		if (jwtGenerator == null) {
+			JwtEncoder jwtEncoder = getJwtEncoder(httpSecurity);
+			if (jwtEncoder != null) {
+				jwtGenerator = new JwtGenerator(jwtEncoder);
+				jwtGenerator.setJwtCustomizer(getJwtCustomizer(httpSecurity));
+				httpSecurity.setSharedObject(JwtGenerator.class, jwtGenerator);
+			}
+		}
+		return jwtGenerator;
+	}
+
+	private static JwtEncoder getJwtEncoder(HttpSecurity httpSecurity) {
+		JwtEncoder jwtEncoder = httpSecurity.getSharedObject(JwtEncoder.class);
+		if (jwtEncoder == null) {
+			jwtEncoder = getOptionalBean(httpSecurity, JwtEncoder.class);
+			if (jwtEncoder == null) {
+				JWKSource<SecurityContext> jwkSource = getJwkSource(httpSecurity);
+				if (jwkSource != null) {
+					jwtEncoder = new NimbusJwtEncoder(jwkSource);
+				}
+			}
+			if (jwtEncoder != null) {
+				httpSecurity.setSharedObject(JwtEncoder.class, jwtEncoder);
+			}
+		}
+		return jwtEncoder;
+	}
+
+	@SuppressWarnings("unchecked")
+	static JWKSource<SecurityContext> getJwkSource(HttpSecurity httpSecurity) {
+		JWKSource<SecurityContext> jwkSource = httpSecurity.getSharedObject(JWKSource.class);
+		if (jwkSource == null) {
+			ResolvableType type = ResolvableType.forClassWithGenerics(JWKSource.class, SecurityContext.class);
+			jwkSource = getOptionalBean(httpSecurity, type);
+			if (jwkSource != null) {
+				httpSecurity.setSharedObject(JWKSource.class, jwkSource);
+			}
+		}
+		return jwkSource;
+	}
+
+	private static OAuth2TokenCustomizer<JwtEncodingContext> getJwtCustomizer(HttpSecurity httpSecurity) {
+		final OAuth2TokenCustomizer<JwtEncodingContext> defaultJwtCustomizer = DefaultOAuth2TokenCustomizers
+			.jwtCustomizer();
+		ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class,
+				JwtEncodingContext.class);
+		final OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = getOptionalBean(httpSecurity, type);
+		if (jwtCustomizer == null) {
+			return defaultJwtCustomizer;
+		}
+		return (context) -> {
+			defaultJwtCustomizer.customize(context);
+			jwtCustomizer.customize(context);
+		};
+	}
+
+	private static OAuth2TokenCustomizer<OAuth2TokenClaimsContext> getAccessTokenCustomizer(HttpSecurity httpSecurity) {
+		final OAuth2TokenCustomizer<OAuth2TokenClaimsContext> defaultAccessTokenCustomizer = DefaultOAuth2TokenCustomizers
+			.accessTokenCustomizer();
+		ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class,
+				OAuth2TokenClaimsContext.class);
+		OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer = getOptionalBean(httpSecurity, type);
+		if (accessTokenCustomizer == null) {
+			return defaultAccessTokenCustomizer;
+		}
+		return (context) -> {
+			defaultAccessTokenCustomizer.customize(context);
+			accessTokenCustomizer.customize(context);
+		};
+	}
+
+	static AuthorizationServerSettings getAuthorizationServerSettings(HttpSecurity httpSecurity) {
+		AuthorizationServerSettings authorizationServerSettings = httpSecurity
+			.getSharedObject(AuthorizationServerSettings.class);
+		if (authorizationServerSettings == null) {
+			authorizationServerSettings = getBean(httpSecurity, AuthorizationServerSettings.class);
+			httpSecurity.setSharedObject(AuthorizationServerSettings.class, authorizationServerSettings);
+		}
+		return authorizationServerSettings;
+	}
+
+	static <T> T getBean(HttpSecurity httpSecurity, Class<T> type) {
+		return httpSecurity.getSharedObject(ApplicationContext.class).getBean(type);
+	}
+
+	@SuppressWarnings("unchecked")
+	static <T> T getBean(HttpSecurity httpSecurity, ResolvableType type) {
+		ApplicationContext context = httpSecurity.getSharedObject(ApplicationContext.class);
+		String[] names = context.getBeanNamesForType(type);
+		if (names.length == 1) {
+			return (T) context.getBean(names[0]);
+		}
+		if (names.length > 1) {
+			throw new NoUniqueBeanDefinitionException(type, names);
+		}
+		throw new NoSuchBeanDefinitionException(type);
+	}
+
+	static <T> T getOptionalBean(HttpSecurity httpSecurity, Class<T> type) {
+		Map<String, T> beansMap = BeanFactoryUtils
+			.beansOfTypeIncludingAncestors(httpSecurity.getSharedObject(ApplicationContext.class), type);
+		if (beansMap.size() > 1) {
+			throw new NoUniqueBeanDefinitionException(type, beansMap.size(),
+					"Expected single matching bean of type '" + type.getName() + "' but found " + beansMap.size() + ": "
+							+ StringUtils.collectionToCommaDelimitedString(beansMap.keySet()));
+		}
+		return (!beansMap.isEmpty() ? beansMap.values().iterator().next() : null);
+	}
+
+	@SuppressWarnings("unchecked")
+	static <T> T getOptionalBean(HttpSecurity httpSecurity, ResolvableType type) {
+		ApplicationContext context = httpSecurity.getSharedObject(ApplicationContext.class);
+		String[] names = context.getBeanNamesForType(type);
+		if (names.length > 1) {
+			throw new NoUniqueBeanDefinitionException(type, names);
+		}
+		return (names.length == 1) ? (T) context.getBean(names[0]) : null;
+	}
+
+}

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

@@ -0,0 +1,273 @@
+/*
+ * Copyright 2020-2025 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.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.endpoint.OAuth2DeviceAuthorizationResponse;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationRequestAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationRequestAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.security.oauth2.server.authorization.web.OAuth2DeviceAuthorizationEndpointFilter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceAuthorizationRequestAuthenticationConverter;
+import org.springframework.security.web.access.intercept.AuthorizationFilter;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
+import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Configurer for the OAuth 2.0 Device Authorization Endpoint.
+ *
+ * @author Steve Riesenberg
+ * @since 1.1
+ * @see OAuth2AuthorizationServerConfigurer#deviceAuthorizationEndpoint
+ * @see OAuth2DeviceAuthorizationEndpointFilter
+ */
+public final class OAuth2DeviceAuthorizationEndpointConfigurer extends AbstractOAuth2Configurer {
+
+	private RequestMatcher requestMatcher;
+
+	private final List<AuthenticationConverter> deviceAuthorizationRequestConverters = new ArrayList<>();
+
+	private Consumer<List<AuthenticationConverter>> deviceAuthorizationRequestConvertersConsumer = (
+			deviceAuthorizationRequestConverters) -> {
+	};
+
+	private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+
+	private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
+	};
+
+	private AuthenticationSuccessHandler deviceAuthorizationResponseHandler;
+
+	private AuthenticationFailureHandler errorResponseHandler;
+
+	private String verificationUri;
+
+	/**
+	 * Restrict for internal use only.
+	 * @param objectPostProcessor an {@code ObjectPostProcessor}
+	 */
+	OAuth2DeviceAuthorizationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
+		super(objectPostProcessor);
+	}
+
+	/**
+	 * Sets the {@link AuthenticationConverter} used when attempting to extract a Device
+	 * Authorization Request from {@link HttpServletRequest} to an instance of
+	 * {@link OAuth2DeviceAuthorizationRequestAuthenticationToken} used for authenticating
+	 * the request.
+	 * @param deviceAuthorizationRequestConverter the {@link AuthenticationConverter} used
+	 * when attempting to extract a Device Authorization Request from
+	 * {@link HttpServletRequest}
+	 * @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2DeviceAuthorizationEndpointConfigurer deviceAuthorizationRequestConverter(
+			AuthenticationConverter deviceAuthorizationRequestConverter) {
+		Assert.notNull(deviceAuthorizationRequestConverter, "deviceAuthorizationRequestConverter cannot be null");
+		this.deviceAuthorizationRequestConverters.add(deviceAuthorizationRequestConverter);
+		return this;
+	}
+
+	/**
+	 * Sets the {@code Consumer} providing access to the {@code List} of default and
+	 * (optionally) added
+	 * {@link #deviceAuthorizationRequestConverter(AuthenticationConverter)
+	 * AuthenticationConverter}'s allowing the ability to add, remove, or customize a
+	 * specific {@link AuthenticationConverter}.
+	 * @param deviceAuthorizationRequestConvertersConsumer the {@code Consumer} providing
+	 * access to the {@code List} of default and (optionally) added
+	 * {@link AuthenticationConverter}'s
+	 * @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2DeviceAuthorizationEndpointConfigurer deviceAuthorizationRequestConverters(
+			Consumer<List<AuthenticationConverter>> deviceAuthorizationRequestConvertersConsumer) {
+		Assert.notNull(deviceAuthorizationRequestConvertersConsumer,
+				"deviceAuthorizationRequestConvertersConsumer cannot be null");
+		this.deviceAuthorizationRequestConvertersConsumer = deviceAuthorizationRequestConvertersConsumer;
+		return this;
+	}
+
+	/**
+	 * Adds an {@link AuthenticationProvider} used for authenticating an
+	 * {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}.
+	 * @param authenticationProvider an {@link AuthenticationProvider} used for
+	 * authenticating an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}
+	 * @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2DeviceAuthorizationEndpointConfigurer authenticationProvider(
+			AuthenticationProvider authenticationProvider) {
+		Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
+		this.authenticationProviders.add(authenticationProvider);
+		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 OAuth2DeviceAuthorizationEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2DeviceAuthorizationEndpointConfigurer 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 OAuth2DeviceAuthorizationRequestAuthenticationToken} and returning the
+	 * {@link OAuth2DeviceAuthorizationResponse Device Authorization Response}.
+	 * @param deviceAuthorizationResponseHandler the {@link AuthenticationSuccessHandler}
+	 * used for handling an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}
+	 * @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2DeviceAuthorizationEndpointConfigurer deviceAuthorizationResponseHandler(
+			AuthenticationSuccessHandler deviceAuthorizationResponseHandler) {
+		this.deviceAuthorizationResponseHandler = deviceAuthorizationResponseHandler;
+		return this;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationFailureHandler} used for handling an
+	 * {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
+	 * Response}.
+	 * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
+	 * handling an {@link OAuth2AuthenticationException}
+	 * @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2DeviceAuthorizationEndpointConfigurer errorResponseHandler(
+			AuthenticationFailureHandler errorResponseHandler) {
+		this.errorResponseHandler = errorResponseHandler;
+		return this;
+	}
+
+	/**
+	 * Sets the end-user verification {@code URI} on the authorization server.
+	 * @param verificationUri the end-user verification {@code URI} on the authorization
+	 * server
+	 * @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2DeviceAuthorizationEndpointConfigurer verificationUri(String verificationUri) {
+		this.verificationUri = verificationUri;
+		return this;
+	}
+
+	@Override
+	public void init(HttpSecurity builder) {
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(builder);
+		String deviceAuthorizationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint())
+				: authorizationServerSettings.getDeviceAuthorizationEndpoint();
+		this.requestMatcher = PathPatternRequestMatcher.withDefaults()
+			.matcher(HttpMethod.POST, deviceAuthorizationEndpointUri);
+
+		List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(builder);
+		if (!this.authenticationProviders.isEmpty()) {
+			authenticationProviders.addAll(0, this.authenticationProviders);
+		}
+		this.authenticationProvidersConsumer.accept(authenticationProviders);
+		authenticationProviders
+			.forEach((authenticationProvider) -> builder.authenticationProvider(postProcess(authenticationProvider)));
+	}
+
+	@Override
+	public void configure(HttpSecurity builder) {
+		AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(builder);
+
+		String deviceAuthorizationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint())
+				: authorizationServerSettings.getDeviceAuthorizationEndpoint();
+		OAuth2DeviceAuthorizationEndpointFilter deviceAuthorizationEndpointFilter = new OAuth2DeviceAuthorizationEndpointFilter(
+				authenticationManager, deviceAuthorizationEndpointUri);
+
+		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
+		if (!this.deviceAuthorizationRequestConverters.isEmpty()) {
+			authenticationConverters.addAll(0, this.deviceAuthorizationRequestConverters);
+		}
+		this.deviceAuthorizationRequestConvertersConsumer.accept(authenticationConverters);
+		deviceAuthorizationEndpointFilter
+			.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
+		if (this.deviceAuthorizationResponseHandler != null) {
+			deviceAuthorizationEndpointFilter.setAuthenticationSuccessHandler(this.deviceAuthorizationResponseHandler);
+		}
+		if (this.errorResponseHandler != null) {
+			deviceAuthorizationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
+		}
+		if (StringUtils.hasText(this.verificationUri)) {
+			deviceAuthorizationEndpointFilter.setVerificationUri(this.verificationUri);
+		}
+		builder.addFilterAfter(postProcess(deviceAuthorizationEndpointFilter), AuthorizationFilter.class);
+	}
+
+	@Override
+	RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+	private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
+		List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
+		authenticationConverters.add(new OAuth2DeviceAuthorizationRequestAuthenticationConverter());
+
+		return authenticationConverters;
+	}
+
+	private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity builder) {
+		List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+
+		OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(builder);
+
+		OAuth2DeviceAuthorizationRequestAuthenticationProvider deviceAuthorizationRequestAuthenticationProvider = new OAuth2DeviceAuthorizationRequestAuthenticationProvider(
+				authorizationService);
+		authenticationProviders.add(deviceAuthorizationRequestAuthenticationProvider);
+
+		return authenticationProviders;
+	}
+
+}

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

@@ -0,0 +1,325 @@
+/*
+ * Copyright 2020-2025 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.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationConsentAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationConsentAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceVerificationAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceVerificationAuthenticationToken;
+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.OAuth2DeviceVerificationEndpointFilter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceAuthorizationConsentAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceVerificationAuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
+import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
+import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
+import org.springframework.security.web.util.matcher.OrRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Configurer for the OAuth 2.0 Device Verification Endpoint.
+ *
+ * @author Steve Riesenberg
+ * @since 1.1
+ * @see OAuth2AuthorizationServerConfigurer#deviceVerificationEndpoint
+ * @see OAuth2DeviceVerificationEndpointFilter
+ */
+public final class OAuth2DeviceVerificationEndpointConfigurer extends AbstractOAuth2Configurer {
+
+	private RequestMatcher requestMatcher;
+
+	private final List<AuthenticationConverter> deviceVerificationRequestConverters = new ArrayList<>();
+
+	private Consumer<List<AuthenticationConverter>> deviceVerificationRequestConvertersConsumer = (
+			deviceVerificationRequestConverters) -> {
+	};
+
+	private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+
+	private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
+	};
+
+	private AuthenticationSuccessHandler deviceVerificationResponseHandler;
+
+	private AuthenticationFailureHandler errorResponseHandler;
+
+	private String consentPage;
+
+	/**
+	 * Restrict for internal use only.
+	 * @param objectPostProcessor an {@code ObjectPostProcessor}
+	 */
+	OAuth2DeviceVerificationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
+		super(objectPostProcessor);
+	}
+
+	/**
+	 * Sets the {@link AuthenticationConverter} used when attempting to extract a Device
+	 * Verification Request (or Device Authorization Consent) from
+	 * {@link HttpServletRequest} to an instance of
+	 * {@link OAuth2DeviceVerificationAuthenticationToken} or
+	 * {@link OAuth2DeviceAuthorizationConsentAuthenticationToken} used for authenticating
+	 * the request.
+	 * @param deviceVerificationRequestConverter the {@link AuthenticationConverter} used
+	 * when attempting to extract a Device Verification Request (or Device Authorization
+	 * Consent) from {@link HttpServletRequest}
+	 * @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2DeviceVerificationEndpointConfigurer deviceVerificationRequestConverter(
+			AuthenticationConverter deviceVerificationRequestConverter) {
+		Assert.notNull(deviceVerificationRequestConverter, "deviceVerificationRequestConverter cannot be null");
+		this.deviceVerificationRequestConverters.add(deviceVerificationRequestConverter);
+		return this;
+	}
+
+	/**
+	 * Sets the {@code Consumer} providing access to the {@code List} of default and
+	 * (optionally) added
+	 * {@link #deviceVerificationRequestConverter(AuthenticationConverter)
+	 * AuthenticationConverter}'s allowing the ability to add, remove, or customize a
+	 * specific {@link AuthenticationConverter}.
+	 * @param deviceVerificationRequestConvertersConsumer the {@code Consumer} providing
+	 * access to the {@code List} of default and (optionally) added
+	 * {@link AuthenticationConverter}'s
+	 * @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2DeviceVerificationEndpointConfigurer deviceVerificationRequestConverters(
+			Consumer<List<AuthenticationConverter>> deviceVerificationRequestConvertersConsumer) {
+		Assert.notNull(deviceVerificationRequestConvertersConsumer,
+				"deviceVerificationRequestConvertersConsumer cannot be null");
+		this.deviceVerificationRequestConvertersConsumer = deviceVerificationRequestConvertersConsumer;
+		return this;
+	}
+
+	/**
+	 * Adds an {@link AuthenticationProvider} used for authenticating an
+	 * {@link OAuth2DeviceVerificationAuthenticationToken} or
+	 * {@link OAuth2DeviceAuthorizationConsentAuthenticationToken}.
+	 * @param authenticationProvider an {@link AuthenticationProvider} used for
+	 * authenticating an {@link OAuth2DeviceVerificationAuthenticationToken} or
+	 * {@link OAuth2DeviceAuthorizationConsentAuthenticationToken}
+	 * @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2DeviceVerificationEndpointConfigurer authenticationProvider(
+			AuthenticationProvider authenticationProvider) {
+		Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
+		this.authenticationProviders.add(authenticationProvider);
+		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 OAuth2DeviceVerificationEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2DeviceVerificationEndpointConfigurer 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 OAuth2DeviceVerificationAuthenticationToken} and returning the response.
+	 * @param deviceVerificationResponseHandler the {@link AuthenticationSuccessHandler}
+	 * used for handling an {@link OAuth2DeviceVerificationAuthenticationToken}
+	 * @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2DeviceVerificationEndpointConfigurer deviceVerificationResponseHandler(
+			AuthenticationSuccessHandler deviceVerificationResponseHandler) {
+		this.deviceVerificationResponseHandler = deviceVerificationResponseHandler;
+		return this;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationFailureHandler} used for handling an
+	 * {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
+	 * Response}.
+	 * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
+	 * handling an {@link OAuth2AuthenticationException}
+	 * @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2DeviceVerificationEndpointConfigurer errorResponseHandler(
+			AuthenticationFailureHandler errorResponseHandler) {
+		this.errorResponseHandler = errorResponseHandler;
+		return this;
+	}
+
+	/**
+	 * Specify the URI to redirect Resource Owners to if consent is required during the
+	 * {@code device_code} flow. A default consent page will be generated when this
+	 * attribute is not specified.
+	 *
+	 * If a URI is specified, applications are required to process the specified URI to
+	 * generate a consent page. The query string will contain the following parameters:
+	 *
+	 * <ul>
+	 * <li>{@code client_id} - the client identifier</li>
+	 * <li>{@code scope} - a space-delimited list of scopes present in the device
+	 * authorization request</li>
+	 * <li>{@code state} - a CSRF protection token</li>
+	 * <li>{@code user_code} - the user code</li>
+	 * </ul>
+	 *
+	 * In general, the consent page should create a form that submits a request with the
+	 * following requirements:
+	 *
+	 * <ul>
+	 * <li>It must be an HTTP POST</li>
+	 * <li>It must be submitted to
+	 * {@link AuthorizationServerSettings#getDeviceVerificationEndpoint()}</li>
+	 * <li>It must include the received {@code client_id} as an HTTP parameter</li>
+	 * <li>It must include the received {@code state} as an HTTP parameter</li>
+	 * <li>It must include the list of {@code scope}s the {@code Resource Owner} consented
+	 * to as an HTTP parameter</li>
+	 * <li>It must include the received {@code user_code} as an HTTP parameter</li>
+	 * </ul>
+	 * @param consentPage the URI of the custom consent page to redirect to if consent is
+	 * required (e.g. "/oauth2/consent")
+	 * @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2DeviceVerificationEndpointConfigurer consentPage(String consentPage) {
+		this.consentPage = consentPage;
+		return this;
+	}
+
+	@Override
+	public void init(HttpSecurity builder) {
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(builder);
+		String deviceVerificationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getDeviceVerificationEndpoint())
+				: authorizationServerSettings.getDeviceVerificationEndpoint();
+		this.requestMatcher = new OrRequestMatcher(
+				PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, deviceVerificationEndpointUri),
+				PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, deviceVerificationEndpointUri));
+
+		List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(builder);
+		if (!this.authenticationProviders.isEmpty()) {
+			authenticationProviders.addAll(0, this.authenticationProviders);
+		}
+		this.authenticationProvidersConsumer.accept(authenticationProviders);
+		authenticationProviders
+			.forEach((authenticationProvider) -> builder.authenticationProvider(postProcess(authenticationProvider)));
+	}
+
+	@Override
+	public void configure(HttpSecurity builder) {
+		AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(builder);
+
+		String deviceVerificationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getDeviceVerificationEndpoint())
+				: authorizationServerSettings.getDeviceVerificationEndpoint();
+		OAuth2DeviceVerificationEndpointFilter deviceVerificationEndpointFilter = new OAuth2DeviceVerificationEndpointFilter(
+				authenticationManager, deviceVerificationEndpointUri);
+		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
+		if (!this.deviceVerificationRequestConverters.isEmpty()) {
+			authenticationConverters.addAll(0, this.deviceVerificationRequestConverters);
+		}
+		this.deviceVerificationRequestConvertersConsumer.accept(authenticationConverters);
+		deviceVerificationEndpointFilter
+			.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
+		if (this.deviceVerificationResponseHandler != null) {
+			deviceVerificationEndpointFilter.setAuthenticationSuccessHandler(this.deviceVerificationResponseHandler);
+		}
+		if (this.errorResponseHandler != null) {
+			deviceVerificationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
+		}
+		if (StringUtils.hasText(this.consentPage)) {
+			deviceVerificationEndpointFilter.setConsentPage(this.consentPage);
+		}
+		builder.addFilterBefore(postProcess(deviceVerificationEndpointFilter),
+				AbstractPreAuthenticatedProcessingFilter.class);
+	}
+
+	@Override
+	RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+	private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
+		List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
+
+		authenticationConverters.add(new OAuth2DeviceVerificationAuthenticationConverter());
+		authenticationConverters.add(new OAuth2DeviceAuthorizationConsentAuthenticationConverter());
+
+		return authenticationConverters;
+	}
+
+	private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity builder) {
+		RegisteredClientRepository registeredClientRepository = OAuth2ConfigurerUtils
+			.getRegisteredClientRepository(builder);
+		OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(builder);
+		OAuth2AuthorizationConsentService authorizationConsentService = OAuth2ConfigurerUtils
+			.getAuthorizationConsentService(builder);
+
+		List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+
+		// @formatter:off
+		OAuth2DeviceVerificationAuthenticationProvider deviceVerificationAuthenticationProvider =
+				new OAuth2DeviceVerificationAuthenticationProvider(
+						registeredClientRepository, authorizationService, authorizationConsentService);
+		// @formatter:on
+		authenticationProviders.add(deviceVerificationAuthenticationProvider);
+
+		// @formatter:off
+		OAuth2DeviceAuthorizationConsentAuthenticationProvider deviceAuthorizationConsentAuthenticationProvider =
+				new OAuth2DeviceAuthorizationConsentAuthenticationProvider(
+						registeredClientRepository, authorizationService, authorizationConsentService);
+		// @formatter:on
+		authenticationProviders.add(deviceAuthorizationConsentAuthenticationProvider);
+
+		return authenticationProviders;
+	}
+
+}

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

@@ -0,0 +1,266 @@
+/*
+ * Copyright 2020-2025 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.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationContext;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationValidator;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.security.oauth2.server.authorization.web.OAuth2PushedAuthorizationRequestEndpointFilter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
+import org.springframework.security.web.access.intercept.AuthorizationFilter;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
+import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+
+/**
+ * Configurer for the OAuth 2.0 Pushed Authorization Request Endpoint.
+ *
+ * @author Joe Grandja
+ * @since 1.5
+ * @see OAuth2AuthorizationServerConfigurer#pushedAuthorizationRequestEndpoint
+ * @see OAuth2PushedAuthorizationRequestEndpointFilter
+ */
+public final class OAuth2PushedAuthorizationRequestEndpointConfigurer extends AbstractOAuth2Configurer {
+
+	private RequestMatcher requestMatcher;
+
+	private final List<AuthenticationConverter> pushedAuthorizationRequestConverters = new ArrayList<>();
+
+	private Consumer<List<AuthenticationConverter>> pushedAuthorizationRequestConvertersConsumer = (
+			authorizationRequestConverters) -> {
+	};
+
+	private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+
+	private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
+	};
+
+	private AuthenticationSuccessHandler pushedAuthorizationResponseHandler;
+
+	private AuthenticationFailureHandler errorResponseHandler;
+
+	private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authorizationCodeRequestAuthenticationValidator;
+
+	/**
+	 * Restrict for internal use only.
+	 * @param objectPostProcessor an {@code ObjectPostProcessor}
+	 */
+	OAuth2PushedAuthorizationRequestEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
+		super(objectPostProcessor);
+	}
+
+	/**
+	 * Adds an {@link AuthenticationConverter} used when attempting to extract a Pushed
+	 * Authorization Request from {@link HttpServletRequest} to an instance of
+	 * {@link OAuth2PushedAuthorizationRequestAuthenticationToken} used for authenticating
+	 * the request.
+	 * @param pushedAuthorizationRequestConverter an {@link AuthenticationConverter} used
+	 * when attempting to extract a Pushed Authorization Request from
+	 * {@link HttpServletRequest}
+	 * @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestConverter(
+			AuthenticationConverter pushedAuthorizationRequestConverter) {
+		Assert.notNull(pushedAuthorizationRequestConverter, "pushedAuthorizationRequestConverter cannot be null");
+		this.pushedAuthorizationRequestConverters.add(pushedAuthorizationRequestConverter);
+		return this;
+	}
+
+	/**
+	 * Sets the {@code Consumer} providing access to the {@code List} of default and
+	 * (optionally) added
+	 * {@link #pushedAuthorizationRequestConverter(AuthenticationConverter)
+	 * AuthenticationConverter}'s allowing the ability to add, remove, or customize a
+	 * specific {@link AuthenticationConverter}.
+	 * @param pushedAuthorizationRequestConvertersConsumer the {@code Consumer} providing
+	 * access to the {@code List} of default and (optionally) added
+	 * {@link AuthenticationConverter}'s
+	 * @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestConverters(
+			Consumer<List<AuthenticationConverter>> pushedAuthorizationRequestConvertersConsumer) {
+		Assert.notNull(pushedAuthorizationRequestConvertersConsumer,
+				"pushedAuthorizationRequestConvertersConsumer cannot be null");
+		this.pushedAuthorizationRequestConvertersConsumer = pushedAuthorizationRequestConvertersConsumer;
+		return this;
+	}
+
+	/**
+	 * Adds an {@link AuthenticationProvider} used for authenticating an
+	 * {@link OAuth2PushedAuthorizationRequestAuthenticationToken}.
+	 * @param authenticationProvider an {@link AuthenticationProvider} used for
+	 * authenticating an {@link OAuth2PushedAuthorizationRequestAuthenticationToken}
+	 * @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2PushedAuthorizationRequestEndpointConfigurer authenticationProvider(
+			AuthenticationProvider authenticationProvider) {
+		Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
+		this.authenticationProviders.add(authenticationProvider);
+		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 OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2PushedAuthorizationRequestEndpointConfigurer 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 OAuth2PushedAuthorizationRequestAuthenticationToken} and returning the
+	 * Pushed Authorization Response.
+	 * @param pushedAuthorizationResponseHandler the {@link AuthenticationSuccessHandler}
+	 * used for handling an {@link OAuth2PushedAuthorizationRequestAuthenticationToken}
+	 * @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationResponseHandler(
+			AuthenticationSuccessHandler pushedAuthorizationResponseHandler) {
+		this.pushedAuthorizationResponseHandler = pushedAuthorizationResponseHandler;
+		return this;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationFailureHandler} used for handling an
+	 * {@link OAuth2AuthorizationCodeRequestAuthenticationException} and returning the
+	 * {@link OAuth2Error Error Response}.
+	 * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
+	 * handling an {@link OAuth2AuthorizationCodeRequestAuthenticationException}
+	 * @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2PushedAuthorizationRequestEndpointConfigurer errorResponseHandler(
+			AuthenticationFailureHandler errorResponseHandler) {
+		this.errorResponseHandler = errorResponseHandler;
+		return this;
+	}
+
+	void addAuthorizationCodeRequestAuthenticationValidator(
+			Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) {
+		this.authorizationCodeRequestAuthenticationValidator = (this.authorizationCodeRequestAuthenticationValidator == null)
+				? authenticationValidator
+				: this.authorizationCodeRequestAuthenticationValidator.andThen(authenticationValidator);
+	}
+
+	@Override
+	void init(HttpSecurity httpSecurity) {
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+		String pushedAuthorizationRequestEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getPushedAuthorizationRequestEndpoint())
+				: authorizationServerSettings.getPushedAuthorizationRequestEndpoint();
+		this.requestMatcher = PathPatternRequestMatcher.withDefaults()
+			.matcher(HttpMethod.POST, pushedAuthorizationRequestEndpointUri);
+		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)));
+	}
+
+	@Override
+	void configure(HttpSecurity httpSecurity) {
+		AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+		String pushedAuthorizationRequestEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getPushedAuthorizationRequestEndpoint())
+				: authorizationServerSettings.getPushedAuthorizationRequestEndpoint();
+		OAuth2PushedAuthorizationRequestEndpointFilter pushedAuthorizationRequestEndpointFilter = new OAuth2PushedAuthorizationRequestEndpointFilter(
+				authenticationManager, pushedAuthorizationRequestEndpointUri);
+		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
+		if (!this.pushedAuthorizationRequestConverters.isEmpty()) {
+			authenticationConverters.addAll(0, this.pushedAuthorizationRequestConverters);
+		}
+		this.pushedAuthorizationRequestConvertersConsumer.accept(authenticationConverters);
+		pushedAuthorizationRequestEndpointFilter
+			.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
+		if (this.pushedAuthorizationResponseHandler != null) {
+			pushedAuthorizationRequestEndpointFilter
+				.setAuthenticationSuccessHandler(this.pushedAuthorizationResponseHandler);
+		}
+		if (this.errorResponseHandler != null) {
+			pushedAuthorizationRequestEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
+		}
+		httpSecurity.addFilterAfter(postProcess(pushedAuthorizationRequestEndpointFilter), AuthorizationFilter.class);
+	}
+
+	@Override
+	RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+	private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
+		List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
+
+		authenticationConverters.add(new OAuth2AuthorizationCodeRequestAuthenticationConverter());
+
+		return authenticationConverters;
+	}
+
+	private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
+		List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+
+		OAuth2PushedAuthorizationRequestAuthenticationProvider pushedAuthorizationRequestAuthenticationProvider = new OAuth2PushedAuthorizationRequestAuthenticationProvider(
+				OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
+		if (this.authorizationCodeRequestAuthenticationValidator != null) {
+			pushedAuthorizationRequestAuthenticationProvider
+				.setAuthenticationValidator(new OAuth2AuthorizationCodeRequestAuthenticationValidator()
+					.andThen(this.authorizationCodeRequestAuthenticationValidator));
+		}
+		authenticationProviders.add(pushedAuthorizationRequestAuthenticationProvider);
+
+		return authenticationProviders;
+	}
+
+}

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

@@ -0,0 +1,280 @@
+/*
+ * Copyright 2020-2025 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.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.core.session.SessionRegistry;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2Token;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+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.OAuth2AuthorizationGrantAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceCodeAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeAuthenticationProvider;
+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.OAuth2AuthorizationCodeAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceCodeAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenExchangeAuthenticationConverter;
+import org.springframework.security.web.access.intercept.AuthorizationFilter;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
+import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+
+/**
+ * Configurer for the OAuth 2.0 Token Endpoint.
+ *
+ * @author Joe Grandja
+ * @since 0.1.2
+ * @see OAuth2AuthorizationServerConfigurer#tokenEndpoint
+ * @see OAuth2TokenEndpointFilter
+ */
+public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configurer {
+
+	private RequestMatcher requestMatcher;
+
+	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;
+
+	/**
+	 * Restrict for internal use only.
+	 * @param objectPostProcessor an {@code ObjectPostProcessor}
+	 */
+	OAuth2TokenEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
+		super(objectPostProcessor);
+	}
+
+	/**
+	 * 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 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) {
+		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;
+	}
+
+	/**
+	 * Adds an {@link AuthenticationProvider} used for authenticating a type of
+	 * {@link OAuth2AuthorizationGrantAuthenticationToken}.
+	 * @param authenticationProvider an {@link AuthenticationProvider} used for
+	 * authenticating a type of {@link OAuth2AuthorizationGrantAuthenticationToken}
+	 * @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
+	 */
+	public OAuth2TokenEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
+		Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
+		this.authenticationProviders.add(authenticationProvider);
+		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}.
+	 * @param accessTokenResponseHandler the {@link AuthenticationSuccessHandler} used for
+	 * handling an {@link OAuth2AccessTokenAuthenticationToken}
+	 * @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
+	 */
+	public OAuth2TokenEndpointConfigurer accessTokenResponseHandler(
+			AuthenticationSuccessHandler accessTokenResponseHandler) {
+		this.accessTokenResponseHandler = accessTokenResponseHandler;
+		return this;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationFailureHandler} used for handling an
+	 * {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
+	 * Response}.
+	 * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
+	 * handling an {@link OAuth2AuthenticationException}
+	 * @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
+	 */
+	public OAuth2TokenEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
+		this.errorResponseHandler = errorResponseHandler;
+		return this;
+	}
+
+	@Override
+	void init(HttpSecurity httpSecurity) {
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+		String tokenEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils.withMultipleIssuersPattern(authorizationServerSettings.getTokenEndpoint())
+				: authorizationServerSettings.getTokenEndpoint();
+		this.requestMatcher = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, tokenEndpointUri);
+
+		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)));
+	}
+
+	@Override
+	void configure(HttpSecurity httpSecurity) {
+		AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+
+		String tokenEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils.withMultipleIssuersPattern(authorizationServerSettings.getTokenEndpoint())
+				: authorizationServerSettings.getTokenEndpoint();
+		OAuth2TokenEndpointFilter tokenEndpointFilter = new OAuth2TokenEndpointFilter(authenticationManager,
+				tokenEndpointUri);
+		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);
+		}
+		if (this.errorResponseHandler != null) {
+			tokenEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
+		}
+		httpSecurity.addFilterAfter(postProcess(tokenEndpointFilter), AuthorizationFilter.class);
+	}
+
+	@Override
+	RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+	private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
+		List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
+
+		authenticationConverters.add(new OAuth2AuthorizationCodeAuthenticationConverter());
+		authenticationConverters.add(new OAuth2RefreshTokenAuthenticationConverter());
+		authenticationConverters.add(new OAuth2ClientCredentialsAuthenticationConverter());
+		authenticationConverters.add(new OAuth2DeviceCodeAuthenticationConverter());
+		authenticationConverters.add(new OAuth2TokenExchangeAuthenticationConverter());
+
+		return authenticationConverters;
+	}
+
+	private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
+		List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+
+		OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity);
+		OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator = OAuth2ConfigurerUtils
+			.getTokenGenerator(httpSecurity);
+
+		OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(
+				authorizationService, tokenGenerator);
+		SessionRegistry sessionRegistry = httpSecurity.getSharedObject(SessionRegistry.class);
+		if (sessionRegistry != null) {
+			authorizationCodeAuthenticationProvider.setSessionRegistry(sessionRegistry);
+		}
+		authenticationProviders.add(authorizationCodeAuthenticationProvider);
+
+		OAuth2RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider = new OAuth2RefreshTokenAuthenticationProvider(
+				authorizationService, tokenGenerator);
+		authenticationProviders.add(refreshTokenAuthenticationProvider);
+
+		OAuth2ClientCredentialsAuthenticationProvider clientCredentialsAuthenticationProvider = new OAuth2ClientCredentialsAuthenticationProvider(
+				authorizationService, tokenGenerator);
+		authenticationProviders.add(clientCredentialsAuthenticationProvider);
+
+		OAuth2DeviceCodeAuthenticationProvider deviceCodeAuthenticationProvider = new OAuth2DeviceCodeAuthenticationProvider(
+				authorizationService, tokenGenerator);
+		authenticationProviders.add(deviceCodeAuthenticationProvider);
+
+		OAuth2TokenExchangeAuthenticationProvider tokenExchangeAuthenticationProvider = new OAuth2TokenExchangeAuthenticationProvider(
+				authorizationService, tokenGenerator);
+		authenticationProviders.add(tokenExchangeAuthenticationProvider);
+
+		return authenticationProviders;
+	}
+
+}

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

@@ -0,0 +1,251 @@
+/*
+ * Copyright 2020-2025 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.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+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.settings.AuthorizationServerSettings;
+import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenIntrospectionAuthenticationConverter;
+import org.springframework.security.web.access.intercept.AuthorizationFilter;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
+import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+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 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;
+
+	/**
+	 * Restrict for internal use only.
+	 * @param objectPostProcessor an {@code ObjectPostProcessor}
+	 */
+	OAuth2TokenIntrospectionEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
+		super(objectPostProcessor);
+	}
+
+	/**
+	 * 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 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) {
+		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;
+	}
+
+	/**
+	 * Adds an {@link AuthenticationProvider} used for authenticating a type of
+	 * {@link OAuth2TokenIntrospectionAuthenticationToken}.
+	 * @param authenticationProvider an {@link AuthenticationProvider} used for
+	 * authenticating a type of {@link OAuth2TokenIntrospectionAuthenticationToken}
+	 * @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2TokenIntrospectionEndpointConfigurer authenticationProvider(
+			AuthenticationProvider authenticationProvider) {
+		Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
+		this.authenticationProviders.add(authenticationProvider);
+		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}.
+	 * @param introspectionResponseHandler the {@link AuthenticationSuccessHandler} used
+	 * for handling an {@link OAuth2TokenIntrospectionAuthenticationToken}
+	 * @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2TokenIntrospectionEndpointConfigurer introspectionResponseHandler(
+			AuthenticationSuccessHandler introspectionResponseHandler) {
+		this.introspectionResponseHandler = introspectionResponseHandler;
+		return this;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationFailureHandler} used for handling an
+	 * {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
+	 * Response}.
+	 * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
+	 * handling an {@link OAuth2AuthenticationException}
+	 * @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2TokenIntrospectionEndpointConfigurer errorResponseHandler(
+			AuthenticationFailureHandler errorResponseHandler) {
+		this.errorResponseHandler = errorResponseHandler;
+		return this;
+	}
+
+	@Override
+	void init(HttpSecurity httpSecurity) {
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+		String tokenIntrospectionEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getTokenIntrospectionEndpoint())
+				: authorizationServerSettings.getTokenIntrospectionEndpoint();
+		this.requestMatcher = PathPatternRequestMatcher.withDefaults()
+			.matcher(HttpMethod.POST, tokenIntrospectionEndpointUri);
+
+		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)));
+	}
+
+	@Override
+	void configure(HttpSecurity httpSecurity) {
+		AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+		String tokenIntrospectionEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getTokenIntrospectionEndpoint())
+				: authorizationServerSettings.getTokenIntrospectionEndpoint();
+		OAuth2TokenIntrospectionEndpointFilter introspectionEndpointFilter = new OAuth2TokenIntrospectionEndpointFilter(
+				authenticationManager, tokenIntrospectionEndpointUri);
+		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);
+		}
+		if (this.errorResponseHandler != null) {
+			introspectionEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
+		}
+		httpSecurity.addFilterAfter(postProcess(introspectionEndpointFilter), AuthorizationFilter.class);
+	}
+
+	@Override
+	RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+	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 = new OAuth2TokenIntrospectionAuthenticationProvider(
+				OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
+				OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
+		authenticationProviders.add(tokenIntrospectionAuthenticationProvider);
+
+		return authenticationProviders;
+	}
+
+}

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

@@ -0,0 +1,250 @@
+/*
+ * Copyright 2020-2025 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.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+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.settings.AuthorizationServerSettings;
+import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenRevocationAuthenticationConverter;
+import org.springframework.security.web.access.intercept.AuthorizationFilter;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
+import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+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 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;
+
+	/**
+	 * Restrict for internal use only.
+	 * @param objectPostProcessor an {@code ObjectPostProcessor}
+	 */
+	OAuth2TokenRevocationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
+		super(objectPostProcessor);
+	}
+
+	/**
+	 * 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 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) {
+		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;
+	}
+
+	/**
+	 * Adds an {@link AuthenticationProvider} used for authenticating a type of
+	 * {@link OAuth2TokenRevocationAuthenticationToken}.
+	 * @param authenticationProvider an {@link AuthenticationProvider} used for
+	 * authenticating a type of {@link OAuth2TokenRevocationAuthenticationToken}
+	 * @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2TokenRevocationEndpointConfigurer authenticationProvider(
+			AuthenticationProvider authenticationProvider) {
+		Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
+		this.authenticationProviders.add(authenticationProvider);
+		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}.
+	 * @param revocationResponseHandler the {@link AuthenticationSuccessHandler} used for
+	 * handling an {@link OAuth2TokenRevocationAuthenticationToken}
+	 * @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2TokenRevocationEndpointConfigurer revocationResponseHandler(
+			AuthenticationSuccessHandler revocationResponseHandler) {
+		this.revocationResponseHandler = revocationResponseHandler;
+		return this;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationFailureHandler} used for handling an
+	 * {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
+	 * Response}.
+	 * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
+	 * handling an {@link OAuth2AuthenticationException}
+	 * @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2TokenRevocationEndpointConfigurer errorResponseHandler(
+			AuthenticationFailureHandler errorResponseHandler) {
+		this.errorResponseHandler = errorResponseHandler;
+		return this;
+	}
+
+	@Override
+	void init(HttpSecurity httpSecurity) {
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+		String tokenRevocationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getTokenRevocationEndpoint())
+				: authorizationServerSettings.getTokenRevocationEndpoint();
+		this.requestMatcher = PathPatternRequestMatcher.withDefaults()
+			.matcher(HttpMethod.POST, tokenRevocationEndpointUri);
+
+		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)));
+	}
+
+	@Override
+	void configure(HttpSecurity httpSecurity) {
+		AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+
+		String tokenRevocationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getTokenRevocationEndpoint())
+				: authorizationServerSettings.getTokenRevocationEndpoint();
+		OAuth2TokenRevocationEndpointFilter revocationEndpointFilter = new OAuth2TokenRevocationEndpointFilter(
+				authenticationManager, tokenRevocationEndpointUri);
+		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);
+		}
+		if (this.errorResponseHandler != null) {
+			revocationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
+		}
+		httpSecurity.addFilterAfter(postProcess(revocationEndpointFilter), AuthorizationFilter.class);
+	}
+
+	@Override
+	RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+	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 = new OAuth2TokenRevocationAuthenticationProvider(
+				OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
+		authenticationProviders.add(tokenRevocationAuthenticationProvider);
+
+		return authenticationProviders;
+	}
+
+}

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

@@ -0,0 +1,274 @@
+/*
+ * Copyright 2020-2025 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.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
+import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientConfigurationAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.oidc.web.OidcClientRegistrationEndpointFilter;
+import org.springframework.security.oauth2.server.authorization.oidc.web.authentication.OidcClientRegistrationAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.security.web.access.intercept.AuthorizationFilter;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
+import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
+import org.springframework.security.web.util.matcher.OrRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+
+/**
+ * Configurer for OpenID Connect 1.0 Dynamic Client Registration Endpoint.
+ *
+ * @author Joe Grandja
+ * @author Daniel Garnier-Moiroux
+ * @since 0.2.0
+ * @see OidcConfigurer#clientRegistrationEndpoint
+ * @see OidcClientRegistrationEndpointFilter
+ */
+public final class OidcClientRegistrationEndpointConfigurer extends AbstractOAuth2Configurer {
+
+	private RequestMatcher requestMatcher;
+
+	private final List<AuthenticationConverter> clientRegistrationRequestConverters = new ArrayList<>();
+
+	private Consumer<List<AuthenticationConverter>> clientRegistrationRequestConvertersConsumer = (
+			clientRegistrationRequestConverters) -> {
+	};
+
+	private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+
+	private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
+	};
+
+	private AuthenticationSuccessHandler clientRegistrationResponseHandler;
+
+	private AuthenticationFailureHandler errorResponseHandler;
+
+	/**
+	 * Restrict for internal use only.
+	 * @param objectPostProcessor an {@code ObjectPostProcessor}
+	 */
+	OidcClientRegistrationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
+		super(objectPostProcessor);
+	}
+
+	/**
+	 * Adds an {@link AuthenticationConverter} used when attempting to extract a Client
+	 * Registration Request from {@link HttpServletRequest} to an instance of
+	 * {@link OidcClientRegistrationAuthenticationToken} used for authenticating the
+	 * request.
+	 * @param clientRegistrationRequestConverter an {@link AuthenticationConverter} used
+	 * when attempting to extract a Client Registration Request from
+	 * {@link HttpServletRequest}
+	 * @return the {@link OidcClientRegistrationEndpointConfigurer} for further
+	 * configuration
+	 * @since 0.4.0
+	 */
+	public OidcClientRegistrationEndpointConfigurer clientRegistrationRequestConverter(
+			AuthenticationConverter clientRegistrationRequestConverter) {
+		Assert.notNull(clientRegistrationRequestConverter, "clientRegistrationRequestConverter cannot be null");
+		this.clientRegistrationRequestConverters.add(clientRegistrationRequestConverter);
+		return this;
+	}
+
+	/**
+	 * Sets the {@code Consumer} providing access to the {@code List} of default and
+	 * (optionally) added
+	 * {@link #clientRegistrationRequestConverter(AuthenticationConverter)
+	 * AuthenticationConverter}'s allowing the ability to add, remove, or customize a
+	 * specific {@link AuthenticationConverter}.
+	 * @param clientRegistrationRequestConvertersConsumer the {@code Consumer} providing
+	 * access to the {@code List} of default and (optionally) added
+	 * {@link AuthenticationConverter}'s
+	 * @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
+	 * @since 0.4.0
+	 */
+	public OidcClientRegistrationEndpointConfigurer clientRegistrationRequestConverters(
+			Consumer<List<AuthenticationConverter>> clientRegistrationRequestConvertersConsumer) {
+		Assert.notNull(clientRegistrationRequestConvertersConsumer,
+				"clientRegistrationRequestConvertersConsumer cannot be null");
+		this.clientRegistrationRequestConvertersConsumer = clientRegistrationRequestConvertersConsumer;
+		return this;
+	}
+
+	/**
+	 * Adds an {@link AuthenticationProvider} used for authenticating an
+	 * {@link OidcClientRegistrationAuthenticationToken}.
+	 * @param authenticationProvider an {@link AuthenticationProvider} used for
+	 * authenticating an {@link OidcClientRegistrationAuthenticationToken}
+	 * @return the {@link OidcClientRegistrationEndpointConfigurer} for further
+	 * configuration
+	 * @since 0.4.0
+	 */
+	public OidcClientRegistrationEndpointConfigurer authenticationProvider(
+			AuthenticationProvider authenticationProvider) {
+		Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
+		this.authenticationProviders.add(authenticationProvider);
+		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 OidcClientRegistrationEndpointConfigurer} for further
+	 * configuration
+	 * @since 0.4.0
+	 */
+	public OidcClientRegistrationEndpointConfigurer 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 OidcClientRegistrationAuthenticationToken} and returning the
+	 * {@link OidcClientRegistration Client Registration Response}.
+	 * @param clientRegistrationResponseHandler the {@link AuthenticationSuccessHandler}
+	 * used for handling an {@link OidcClientRegistrationAuthenticationToken}
+	 * @return the {@link OidcClientRegistrationEndpointConfigurer} for further
+	 * configuration
+	 * @since 0.4.0
+	 */
+	public OidcClientRegistrationEndpointConfigurer clientRegistrationResponseHandler(
+			AuthenticationSuccessHandler clientRegistrationResponseHandler) {
+		this.clientRegistrationResponseHandler = clientRegistrationResponseHandler;
+		return this;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationFailureHandler} used for handling an
+	 * {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
+	 * Response}.
+	 * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
+	 * handling an {@link OAuth2AuthenticationException}
+	 * @return the {@link OidcClientRegistrationEndpointConfigurer} for further
+	 * configuration
+	 * @since 0.4.0
+	 */
+	public OidcClientRegistrationEndpointConfigurer errorResponseHandler(
+			AuthenticationFailureHandler errorResponseHandler) {
+		this.errorResponseHandler = errorResponseHandler;
+		return this;
+	}
+
+	@Override
+	void init(HttpSecurity httpSecurity) {
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+		String clientRegistrationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getOidcClientRegistrationEndpoint())
+				: authorizationServerSettings.getOidcClientRegistrationEndpoint();
+		this.requestMatcher = new OrRequestMatcher(
+				PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, clientRegistrationEndpointUri),
+				PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, clientRegistrationEndpointUri));
+
+		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)));
+	}
+
+	@Override
+	void configure(HttpSecurity httpSecurity) {
+		AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+
+		String clientRegistrationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getOidcClientRegistrationEndpoint())
+				: authorizationServerSettings.getOidcClientRegistrationEndpoint();
+		OidcClientRegistrationEndpointFilter oidcClientRegistrationEndpointFilter = new OidcClientRegistrationEndpointFilter(
+				authenticationManager, clientRegistrationEndpointUri);
+		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
+		if (!this.clientRegistrationRequestConverters.isEmpty()) {
+			authenticationConverters.addAll(0, this.clientRegistrationRequestConverters);
+		}
+		this.clientRegistrationRequestConvertersConsumer.accept(authenticationConverters);
+		oidcClientRegistrationEndpointFilter
+			.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
+		if (this.clientRegistrationResponseHandler != null) {
+			oidcClientRegistrationEndpointFilter
+				.setAuthenticationSuccessHandler(this.clientRegistrationResponseHandler);
+		}
+		if (this.errorResponseHandler != null) {
+			oidcClientRegistrationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
+		}
+		httpSecurity.addFilterAfter(postProcess(oidcClientRegistrationEndpointFilter), AuthorizationFilter.class);
+	}
+
+	@Override
+	RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+	private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
+		List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
+
+		authenticationConverters.add(new OidcClientRegistrationAuthenticationConverter());
+
+		return authenticationConverters;
+	}
+
+	private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
+		List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+
+		OidcClientRegistrationAuthenticationProvider oidcClientRegistrationAuthenticationProvider = new OidcClientRegistrationAuthenticationProvider(
+				OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
+				OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity),
+				OAuth2ConfigurerUtils.getTokenGenerator(httpSecurity));
+		PasswordEncoder passwordEncoder = OAuth2ConfigurerUtils.getOptionalBean(httpSecurity, PasswordEncoder.class);
+		if (passwordEncoder != null) {
+			oidcClientRegistrationAuthenticationProvider.setPasswordEncoder(passwordEncoder);
+		}
+		authenticationProviders.add(oidcClientRegistrationAuthenticationProvider);
+
+		OidcClientConfigurationAuthenticationProvider oidcClientConfigurationAuthenticationProvider = new OidcClientConfigurationAuthenticationProvider(
+				OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
+				OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
+		authenticationProviders.add(oidcClientConfigurationAuthenticationProvider);
+
+		return authenticationProviders;
+	}
+
+}

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

@@ -0,0 +1,168 @@
+/*
+ * Copyright 2020-2025 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.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.security.web.util.matcher.OrRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.web.util.UriComponentsBuilder;
+
+/**
+ * Configurer for OpenID Connect 1.0 support.
+ *
+ * @author Joe Grandja
+ * @since 0.2.0
+ * @see OAuth2AuthorizationServerConfigurer#oidc
+ * @see OidcProviderConfigurationEndpointConfigurer
+ * @see OidcLogoutEndpointConfigurer
+ * @see OidcClientRegistrationEndpointConfigurer
+ * @see OidcUserInfoEndpointConfigurer
+ */
+public final class OidcConfigurer extends AbstractOAuth2Configurer {
+
+	private final Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = new LinkedHashMap<>();
+
+	private RequestMatcher requestMatcher;
+
+	/**
+	 * Restrict for internal use only.
+	 * @param objectPostProcessor an {@code ObjectPostProcessor}
+	 */
+	OidcConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
+		super(objectPostProcessor);
+		addConfigurer(OidcProviderConfigurationEndpointConfigurer.class,
+				new OidcProviderConfigurationEndpointConfigurer(objectPostProcessor));
+		addConfigurer(OidcLogoutEndpointConfigurer.class, new OidcLogoutEndpointConfigurer(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 1.0 RP-Initiated Logout Endpoint.
+	 * @param logoutEndpointCustomizer the {@link Customizer} providing access to the
+	 * {@link OidcLogoutEndpointConfigurer}
+	 * @return the {@link OidcConfigurer} for further configuration
+	 * @since 1.1
+	 */
+	public OidcConfigurer logoutEndpoint(Customizer<OidcLogoutEndpointConfigurer> logoutEndpointCustomizer) {
+		logoutEndpointCustomizer.customize(getConfigurer(OidcLogoutEndpointConfigurer.class));
+		return this;
+	}
+
+	/**
+	 * Configures the OpenID Connect Dynamic Client Registration 1.0 Endpoint.
+	 * @param clientRegistrationEndpointCustomizer the {@link Customizer} providing access
+	 * to the {@link OidcClientRegistrationEndpointConfigurer}
+	 * @return the {@link OidcConfigurer} for further configuration
+	 */
+	public OidcConfigurer clientRegistrationEndpoint(
+			Customizer<OidcClientRegistrationEndpointConfigurer> clientRegistrationEndpointCustomizer) {
+		OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer = getConfigurer(
+				OidcClientRegistrationEndpointConfigurer.class);
+		if (clientRegistrationEndpointConfigurer == null) {
+			addConfigurer(OidcClientRegistrationEndpointConfigurer.class,
+					new OidcClientRegistrationEndpointConfigurer(getObjectPostProcessor()));
+			clientRegistrationEndpointConfigurer = getConfigurer(OidcClientRegistrationEndpointConfigurer.class);
+		}
+		clientRegistrationEndpointCustomizer.customize(clientRegistrationEndpointConfigurer);
+		return this;
+	}
+
+	/**
+	 * Configures the OpenID Connect 1.0 UserInfo Endpoint.
+	 * @param userInfoEndpointCustomizer the {@link Customizer} providing access to the
+	 * {@link OidcUserInfoEndpointConfigurer}
+	 * @return the {@link OidcConfigurer} for further configuration
+	 */
+	public OidcConfigurer userInfoEndpoint(Customizer<OidcUserInfoEndpointConfigurer> userInfoEndpointCustomizer) {
+		userInfoEndpointCustomizer.customize(getConfigurer(OidcUserInfoEndpointConfigurer.class));
+		return this;
+	}
+
+	@Override
+	void init(HttpSecurity httpSecurity) {
+		List<RequestMatcher> requestMatchers = new ArrayList<>();
+		this.configurers.values().forEach((configurer) -> {
+			configurer.init(httpSecurity);
+			requestMatchers.add(configurer.getRequestMatcher());
+		});
+		this.requestMatcher = new OrRequestMatcher(requestMatchers);
+	}
+
+	@Override
+	void configure(HttpSecurity httpSecurity) {
+		OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer = getConfigurer(
+				OidcClientRegistrationEndpointConfigurer.class);
+		if (clientRegistrationEndpointConfigurer != null) {
+			OidcProviderConfigurationEndpointConfigurer providerConfigurationEndpointConfigurer = getConfigurer(
+					OidcProviderConfigurationEndpointConfigurer.class);
+
+			providerConfigurationEndpointConfigurer.addDefaultProviderConfigurationCustomizer((builder) -> {
+				AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
+				String issuer = authorizationServerContext.getIssuer();
+				AuthorizationServerSettings authorizationServerSettings = authorizationServerContext
+					.getAuthorizationServerSettings();
+
+				String clientRegistrationEndpoint = UriComponentsBuilder.fromUriString(issuer)
+					.path(authorizationServerSettings.getOidcClientRegistrationEndpoint())
+					.build()
+					.toUriString();
+
+				builder.clientRegistrationEndpoint(clientRegistrationEndpoint);
+			});
+		}
+
+		this.configurers.values().forEach((configurer) -> configurer.configure(httpSecurity));
+	}
+
+	@Override
+	RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+	@SuppressWarnings("unchecked")
+	<T> T getConfigurer(Class<T> type) {
+		return (T) this.configurers.get(type);
+	}
+
+	private <T extends AbstractOAuth2Configurer> void addConfigurer(Class<T> configurerType, T configurer) {
+		this.configurers.put(configurerType, configurer);
+	}
+
+}

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

@@ -0,0 +1,237 @@
+/*
+ * Copyright 2020-2025 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.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.core.session.SessionRegistry;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.oidc.web.OidcLogoutEndpointFilter;
+import org.springframework.security.oauth2.server.authorization.oidc.web.authentication.OidcLogoutAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
+import org.springframework.security.web.authentication.logout.LogoutFilter;
+import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
+import org.springframework.security.web.util.matcher.OrRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+
+/**
+ * Configurer for OpenID Connect 1.0 RP-Initiated Logout Endpoint.
+ *
+ * @author Joe Grandja
+ * @since 1.1
+ * @see OidcConfigurer#logoutEndpoint
+ * @see OidcLogoutEndpointFilter
+ */
+public final class OidcLogoutEndpointConfigurer extends AbstractOAuth2Configurer {
+
+	private RequestMatcher requestMatcher;
+
+	private final List<AuthenticationConverter> logoutRequestConverters = new ArrayList<>();
+
+	private Consumer<List<AuthenticationConverter>> logoutRequestConvertersConsumer = (logoutRequestConverters) -> {
+	};
+
+	private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+
+	private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
+	};
+
+	private AuthenticationSuccessHandler logoutResponseHandler;
+
+	private AuthenticationFailureHandler errorResponseHandler;
+
+	/**
+	 * Restrict for internal use only.
+	 * @param objectPostProcessor an {@code ObjectPostProcessor}
+	 */
+	OidcLogoutEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
+		super(objectPostProcessor);
+	}
+
+	/**
+	 * Adds an {@link AuthenticationConverter} used when attempting to extract a Logout
+	 * Request from {@link HttpServletRequest} to an instance of
+	 * {@link OidcLogoutAuthenticationToken} used for authenticating the request.
+	 * @param logoutRequestConverter an {@link AuthenticationConverter} used when
+	 * attempting to extract a Logout Request from {@link HttpServletRequest}
+	 * @return the {@link OidcLogoutEndpointConfigurer} for further configuration
+	 */
+	public OidcLogoutEndpointConfigurer logoutRequestConverter(AuthenticationConverter logoutRequestConverter) {
+		Assert.notNull(logoutRequestConverter, "logoutRequestConverter cannot be null");
+		this.logoutRequestConverters.add(logoutRequestConverter);
+		return this;
+	}
+
+	/**
+	 * Sets the {@code Consumer} providing access to the {@code List} of default and
+	 * (optionally) added {@link #logoutRequestConverter(AuthenticationConverter)
+	 * AuthenticationConverter}'s allowing the ability to add, remove, or customize a
+	 * specific {@link AuthenticationConverter}.
+	 * @param logoutRequestConvertersConsumer the {@code Consumer} providing access to the
+	 * {@code List} of default and (optionally) added {@link AuthenticationConverter}'s
+	 * @return the {@link OidcLogoutEndpointConfigurer} for further configuration
+	 */
+	public OidcLogoutEndpointConfigurer logoutRequestConverters(
+			Consumer<List<AuthenticationConverter>> logoutRequestConvertersConsumer) {
+		Assert.notNull(logoutRequestConvertersConsumer, "logoutRequestConvertersConsumer cannot be null");
+		this.logoutRequestConvertersConsumer = logoutRequestConvertersConsumer;
+		return this;
+	}
+
+	/**
+	 * Adds an {@link AuthenticationProvider} used for authenticating an
+	 * {@link OidcLogoutAuthenticationToken}.
+	 * @param authenticationProvider an {@link AuthenticationProvider} used for
+	 * authenticating an {@link OidcLogoutAuthenticationToken}
+	 * @return the {@link OidcLogoutEndpointConfigurer} for further configuration
+	 */
+	public OidcLogoutEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
+		Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
+		this.authenticationProviders.add(authenticationProvider);
+		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 OidcLogoutEndpointConfigurer} for further configuration
+	 */
+	public OidcLogoutEndpointConfigurer 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 OidcLogoutAuthenticationToken} and performing the logout.
+	 * @param logoutResponseHandler the {@link AuthenticationSuccessHandler} used for
+	 * handling an {@link OidcLogoutAuthenticationToken}
+	 * @return the {@link OidcLogoutEndpointConfigurer} for further configuration
+	 */
+	public OidcLogoutEndpointConfigurer logoutResponseHandler(AuthenticationSuccessHandler logoutResponseHandler) {
+		this.logoutResponseHandler = logoutResponseHandler;
+		return this;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationFailureHandler} used for handling an
+	 * {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
+	 * Response}.
+	 * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
+	 * handling an {@link OAuth2AuthenticationException}
+	 * @return the {@link OidcLogoutEndpointConfigurer} for further configuration
+	 */
+	public OidcLogoutEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
+		this.errorResponseHandler = errorResponseHandler;
+		return this;
+	}
+
+	@Override
+	void init(HttpSecurity httpSecurity) {
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+		String logoutEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils.withMultipleIssuersPattern(authorizationServerSettings.getOidcLogoutEndpoint())
+				: authorizationServerSettings.getOidcLogoutEndpoint();
+		this.requestMatcher = new OrRequestMatcher(
+				PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, logoutEndpointUri),
+				PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, logoutEndpointUri));
+
+		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)));
+	}
+
+	@Override
+	void configure(HttpSecurity httpSecurity) {
+		AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+
+		String logoutEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils.withMultipleIssuersPattern(authorizationServerSettings.getOidcLogoutEndpoint())
+				: authorizationServerSettings.getOidcLogoutEndpoint();
+		OidcLogoutEndpointFilter oidcLogoutEndpointFilter = new OidcLogoutEndpointFilter(authenticationManager,
+				logoutEndpointUri);
+		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
+		if (!this.logoutRequestConverters.isEmpty()) {
+			authenticationConverters.addAll(0, this.logoutRequestConverters);
+		}
+		this.logoutRequestConvertersConsumer.accept(authenticationConverters);
+		oidcLogoutEndpointFilter
+			.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
+		if (this.logoutResponseHandler != null) {
+			oidcLogoutEndpointFilter.setAuthenticationSuccessHandler(this.logoutResponseHandler);
+		}
+		if (this.errorResponseHandler != null) {
+			oidcLogoutEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
+		}
+		httpSecurity.addFilterBefore(postProcess(oidcLogoutEndpointFilter), LogoutFilter.class);
+	}
+
+	@Override
+	RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+	private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
+		List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
+
+		authenticationConverters.add(new OidcLogoutAuthenticationConverter());
+
+		return authenticationConverters;
+	}
+
+	private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
+		List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+
+		OidcLogoutAuthenticationProvider oidcLogoutAuthenticationProvider = new OidcLogoutAuthenticationProvider(
+				OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
+				OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity),
+				httpSecurity.getSharedObject(SessionRegistry.class));
+		authenticationProviders.add(oidcLogoutAuthenticationProvider);
+
+		return authenticationProviders;
+	}
+
+}

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

@@ -0,0 +1,117 @@
+/*
+ * Copyright 2020-2025 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.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.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
+import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
+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.
+	 * @param objectPostProcessor an {@code ObjectPostProcessor}
+	 */
+	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) {
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+		String oidcProviderConfigurationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? "/**/.well-known/openid-configuration" : "/.well-known/openid-configuration";
+		this.requestMatcher = PathPatternRequestMatcher.withDefaults()
+			.matcher(HttpMethod.GET, oidcProviderConfigurationEndpointUri);
+	}
+
+	@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)
+						? providerConfigurationCustomizer.andThen(this.providerConfigurationCustomizer)
+						: this.providerConfigurationCustomizer;
+			}
+		}
+		return providerConfigurationCustomizer;
+	}
+
+	@Override
+	RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+}

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

@@ -0,0 +1,286 @@
+/*
+ * Copyright 2020-2025 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.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
+import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext;
+import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.oidc.web.OidcUserInfoEndpointFilter;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.security.web.access.intercept.AuthorizationFilter;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
+import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
+import org.springframework.security.web.util.matcher.OrRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+
+/**
+ * Configurer for OpenID Connect 1.0 UserInfo Endpoint.
+ *
+ * @author Steve Riesenberg
+ * @author Daniel Garnier-Moiroux
+ * @since 0.2.1
+ * @see OidcConfigurer#userInfoEndpoint
+ * @see OidcUserInfoEndpointFilter
+ */
+public final class OidcUserInfoEndpointConfigurer extends AbstractOAuth2Configurer {
+
+	private RequestMatcher requestMatcher;
+
+	private final List<AuthenticationConverter> userInfoRequestConverters = new ArrayList<>();
+
+	private Consumer<List<AuthenticationConverter>> userInfoRequestConvertersConsumer = (userInfoRequestConverters) -> {
+	};
+
+	private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+
+	private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
+	};
+
+	private AuthenticationSuccessHandler userInfoResponseHandler;
+
+	private AuthenticationFailureHandler errorResponseHandler;
+
+	private Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper;
+
+	/**
+	 * Restrict for internal use only.
+	 * @param objectPostProcessor an {@code ObjectPostProcessor}
+	 */
+	OidcUserInfoEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
+		super(objectPostProcessor);
+	}
+
+	/**
+	 * Adds an {@link AuthenticationConverter} used when attempting to extract an UserInfo
+	 * Request from {@link HttpServletRequest} to an instance of
+	 * {@link OidcUserInfoAuthenticationToken} used for authenticating the request.
+	 * @param userInfoRequestConverter an {@link AuthenticationConverter} used when
+	 * attempting to extract an UserInfo Request from {@link HttpServletRequest}
+	 * @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
+	 * @since 0.4.0
+	 */
+	public OidcUserInfoEndpointConfigurer userInfoRequestConverter(AuthenticationConverter userInfoRequestConverter) {
+		Assert.notNull(userInfoRequestConverter, "userInfoRequestConverter cannot be null");
+		this.userInfoRequestConverters.add(userInfoRequestConverter);
+		return this;
+	}
+
+	/**
+	 * Sets the {@code Consumer} providing access to the {@code List} of default and
+	 * (optionally) added {@link #userInfoRequestConverter(AuthenticationConverter)
+	 * AuthenticationConverter}'s allowing the ability to add, remove, or customize a
+	 * specific {@link AuthenticationConverter}.
+	 * @param userInfoRequestConvertersConsumer the {@code Consumer} providing access to
+	 * the {@code List} of default and (optionally) added
+	 * {@link AuthenticationConverter}'s
+	 * @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
+	 * @since 0.4.0
+	 */
+	public OidcUserInfoEndpointConfigurer userInfoRequestConverters(
+			Consumer<List<AuthenticationConverter>> userInfoRequestConvertersConsumer) {
+		Assert.notNull(userInfoRequestConvertersConsumer, "userInfoRequestConvertersConsumer cannot be null");
+		this.userInfoRequestConvertersConsumer = userInfoRequestConvertersConsumer;
+		return this;
+	}
+
+	/**
+	 * Adds an {@link AuthenticationProvider} used for authenticating an
+	 * {@link OidcUserInfoAuthenticationToken}.
+	 * @param authenticationProvider an {@link AuthenticationProvider} used for
+	 * authenticating an {@link OidcUserInfoAuthenticationToken}
+	 * @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
+	 * @since 0.4.0
+	 */
+	public OidcUserInfoEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
+		Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
+		this.authenticationProviders.add(authenticationProvider);
+		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 OidcUserInfoEndpointConfigurer} for further configuration
+	 * @since 0.4.0
+	 */
+	public OidcUserInfoEndpointConfigurer 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 OidcUserInfoAuthenticationToken} and returning the {@link OidcUserInfo
+	 * UserInfo Response}.
+	 * @param userInfoResponseHandler the {@link AuthenticationSuccessHandler} used for
+	 * handling an {@link OidcUserInfoAuthenticationToken}
+	 * @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
+	 * @since 0.4.0
+	 */
+	public OidcUserInfoEndpointConfigurer userInfoResponseHandler(
+			AuthenticationSuccessHandler userInfoResponseHandler) {
+		this.userInfoResponseHandler = userInfoResponseHandler;
+		return this;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationFailureHandler} used for handling an
+	 * {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
+	 * Response}.
+	 * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
+	 * handling an {@link OAuth2AuthenticationException}
+	 * @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
+	 * @since 0.4.0
+	 */
+	public OidcUserInfoEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
+		this.errorResponseHandler = errorResponseHandler;
+		return this;
+	}
+
+	/**
+	 * Sets the {@link Function} used to extract claims from
+	 * {@link OidcUserInfoAuthenticationContext} to an instance of {@link OidcUserInfo}
+	 * for the UserInfo response.
+	 *
+	 * <p>
+	 * The {@link OidcUserInfoAuthenticationContext} gives the mapper access to the
+	 * {@link OidcUserInfoAuthenticationToken}, as well as, the following context
+	 * attributes:
+	 * <ul>
+	 * <li>{@link OidcUserInfoAuthenticationContext#getAccessToken()} containing the
+	 * bearer token used to make the request.</li>
+	 * <li>{@link OidcUserInfoAuthenticationContext#getAuthorization()} containing the
+	 * {@link OidcIdToken} and {@link OAuth2AccessToken} associated with the bearer token
+	 * used to make the request.</li>
+	 * </ul>
+	 * @param userInfoMapper the {@link Function} used to extract claims from
+	 * {@link OidcUserInfoAuthenticationContext} to an instance of {@link OidcUserInfo}
+	 * @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
+	 */
+	public OidcUserInfoEndpointConfigurer userInfoMapper(
+			Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper) {
+		this.userInfoMapper = userInfoMapper;
+		return this;
+	}
+
+	@Override
+	void init(HttpSecurity httpSecurity) {
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+		String userInfoEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getOidcUserInfoEndpoint())
+				: authorizationServerSettings.getOidcUserInfoEndpoint();
+		this.requestMatcher = new OrRequestMatcher(
+				PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, userInfoEndpointUri),
+				PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, userInfoEndpointUri));
+
+		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)));
+	}
+
+	@Override
+	void configure(HttpSecurity httpSecurity) {
+		AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+
+		String userInfoEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getOidcUserInfoEndpoint())
+				: authorizationServerSettings.getOidcUserInfoEndpoint();
+		OidcUserInfoEndpointFilter oidcUserInfoEndpointFilter = new OidcUserInfoEndpointFilter(authenticationManager,
+				userInfoEndpointUri);
+		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
+		if (!this.userInfoRequestConverters.isEmpty()) {
+			authenticationConverters.addAll(0, this.userInfoRequestConverters);
+		}
+		this.userInfoRequestConvertersConsumer.accept(authenticationConverters);
+		oidcUserInfoEndpointFilter
+			.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
+		if (this.userInfoResponseHandler != null) {
+			oidcUserInfoEndpointFilter.setAuthenticationSuccessHandler(this.userInfoResponseHandler);
+		}
+		if (this.errorResponseHandler != null) {
+			oidcUserInfoEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
+		}
+		httpSecurity.addFilterAfter(postProcess(oidcUserInfoEndpointFilter), AuthorizationFilter.class);
+	}
+
+	@Override
+	RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+	private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
+		List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
+
+		authenticationConverters.add((request) -> {
+			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+			return new OidcUserInfoAuthenticationToken(authentication);
+		});
+
+		return authenticationConverters;
+	}
+
+	private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
+		List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+
+		OidcUserInfoAuthenticationProvider oidcUserInfoAuthenticationProvider = new OidcUserInfoAuthenticationProvider(
+				OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
+		if (this.userInfoMapper != null) {
+			oidcUserInfoAuthenticationProvider.setUserInfoMapper(this.userInfoMapper);
+		}
+		authenticationProviders.add(oidcUserInfoAuthenticationProvider);
+
+		return authenticationProviders;
+	}
+
+}

+ 62 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/context/AuthorizationServerContext.java

@@ -0,0 +1,62 @@
+/*
+ * Copyright 2020-2024 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.context;
+
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+
+/**
+ * A context that holds information of the Authorization Server runtime environment.
+ *
+ * @author Joe Grandja
+ * @since 0.2.2
+ * @see AuthorizationServerSettings
+ * @see AuthorizationServerContextHolder
+ */
+public interface AuthorizationServerContext {
+
+	/**
+	 * Returns {@link AuthorizationServerSettings#getIssuer()} if available, otherwise,
+	 * resolves the issuer identifier from the <i>"current"</i> request.
+	 *
+	 * <p>
+	 * The issuer identifier may contain a path component to support
+	 * {@link AuthorizationServerSettings#isMultipleIssuersAllowed() multiple issuers per
+	 * host} in a multi-tenant hosting configuration.
+	 *
+	 * <p>
+	 * For example:
+	 * <ul>
+	 * <li>{@code https://example.com/issuer1/oauth2/token} &mdash; resolves the issuer to
+	 * {@code https://example.com/issuer1}</li>
+	 * <li>{@code https://example.com/issuer2/oauth2/token} &mdash; resolves the issuer to
+	 * {@code https://example.com/issuer2}</li>
+	 * <li>{@code https://example.com/authz/issuer1/oauth2/token} &mdash; resolves the
+	 * issuer to {@code https://example.com/authz/issuer1}</li>
+	 * <li>{@code https://example.com/authz/issuer2/oauth2/token} &mdash; resolves the
+	 * issuer to {@code https://example.com/authz/issuer2}</li>
+	 * </ul>
+	 * @return {@link AuthorizationServerSettings#getIssuer()} if available, otherwise,
+	 * resolves the issuer identifier from the <i>"current"</i> request
+	 */
+	String getIssuer();
+
+	/**
+	 * Returns the {@link AuthorizationServerSettings}.
+	 * @return the {@link AuthorizationServerSettings}
+	 */
+	AuthorizationServerSettings getAuthorizationServerSettings();
+
+}

+ 61 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/context/AuthorizationServerContextHolder.java

@@ -0,0 +1,61 @@
+/*
+ * 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.context;
+
+/**
+ * A holder of the {@link AuthorizationServerContext} that associates it with the current
+ * thread using a {@code ThreadLocal}.
+ *
+ * @author Joe Grandja
+ * @since 0.2.2
+ * @see AuthorizationServerContext
+ */
+public final class AuthorizationServerContextHolder {
+
+	private static final ThreadLocal<AuthorizationServerContext> holder = new ThreadLocal<>();
+
+	private AuthorizationServerContextHolder() {
+	}
+
+	/**
+	 * Returns the {@link AuthorizationServerContext} bound to the current thread.
+	 * @return the {@link AuthorizationServerContext}
+	 */
+	public static AuthorizationServerContext getContext() {
+		return holder.get();
+	}
+
+	/**
+	 * Bind the given {@link AuthorizationServerContext} to the current thread.
+	 * @param authorizationServerContext the {@link AuthorizationServerContext}
+	 */
+	public static void setContext(AuthorizationServerContext authorizationServerContext) {
+		if (authorizationServerContext == null) {
+			resetContext();
+		}
+		else {
+			holder.set(authorizationServerContext);
+		}
+	}
+
+	/**
+	 * Reset the {@link AuthorizationServerContext} bound to the current thread.
+	 */
+	public static void resetContext() {
+		holder.remove();
+	}
+
+}

+ 62 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/context/Context.java

@@ -0,0 +1,62 @@
+/*
+ * 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.context;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * A facility for holding information associated to a specific context.
+ *
+ * @author Joe Grandja
+ * @since 0.1.0
+ */
+public interface Context {
+
+	/**
+	 * Returns the value of the attribute associated to the key.
+	 * @param key the key for the attribute
+	 * @param <V> the type of the value for the attribute
+	 * @return the value of the attribute associated to the key, or {@code null} if not
+	 * available
+	 */
+	@Nullable
+	<V> V get(Object key);
+
+	/**
+	 * Returns the value of the attribute associated to the key.
+	 * @param key the key for the attribute
+	 * @param <V> the type of the value for the attribute
+	 * @return the value of the attribute associated to the key, or {@code null} if not
+	 * available or not of the specified type
+	 */
+	@Nullable
+	default <V> V get(Class<V> key) {
+		Assert.notNull(key, "key cannot be null");
+		V value = get((Object) key);
+		return key.isInstance(value) ? value : null;
+	}
+
+	/**
+	 * Returns {@code true} if an attribute associated to the key exists, {@code false}
+	 * otherwise.
+	 * @param key the key for the attribute
+	 * @return {@code true} if an attribute associated to the key exists, {@code false}
+	 * otherwise
+	 */
+	boolean hasKey(Object key);
+
+}

+ 64 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/http/converter/HttpMessageConverters.java

@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020-2023 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.http.converter;
+
+import org.springframework.http.converter.GenericHttpMessageConverter;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.json.GsonHttpMessageConverter;
+import org.springframework.http.converter.json.JsonbHttpMessageConverter;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.util.ClassUtils;
+
+/**
+ * Utility methods for {@link HttpMessageConverter}'s.
+ *
+ * @author Joe Grandja
+ * @author l uamas
+ * @since 0.1.1
+ */
+final class HttpMessageConverters {
+
+	private static final boolean jackson2Present;
+
+	private static final boolean gsonPresent;
+
+	private static final boolean jsonbPresent;
+
+	static {
+		ClassLoader classLoader = HttpMessageConverters.class.getClassLoader();
+		jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
+				&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
+		gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
+		jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader);
+	}
+
+	private HttpMessageConverters() {
+	}
+
+	static GenericHttpMessageConverter<Object> getJsonMessageConverter() {
+		if (jackson2Present) {
+			return new MappingJackson2HttpMessageConverter();
+		}
+		if (gsonPresent) {
+			return new GsonHttpMessageConverter();
+		}
+		if (jsonbPresent) {
+			return new JsonbHttpMessageConverter();
+		}
+		return null;
+	}
+
+}

+ 191 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/http/converter/OAuth2AuthorizationServerMetadataHttpMessageConverter.java

@@ -0,0 +1,191 @@
+/*
+ * Copyright 2020-2025 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.http.converter;
+
+import java.net.URL;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.core.convert.TypeDescriptor;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.HttpOutputMessage;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.AbstractHttpMessageConverter;
+import org.springframework.http.converter.GenericHttpMessageConverter;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.http.converter.HttpMessageNotWritableException;
+import org.springframework.security.oauth2.core.converter.ClaimConversionService;
+import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadataClaimNames;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link HttpMessageConverter} for an {@link OAuth2AuthorizationServerMetadata OAuth
+ * 2.0 Authorization Server Metadata Response}.
+ *
+ * @author Daniel Garnier-Moiroux
+ * @since 0.1.1
+ * @see AbstractHttpMessageConverter
+ * @see OAuth2AuthorizationServerMetadata
+ */
+public class OAuth2AuthorizationServerMetadataHttpMessageConverter
+		extends AbstractHttpMessageConverter<OAuth2AuthorizationServerMetadata> {
+
+	private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() {
+	};
+
+	private final GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters
+		.getJsonMessageConverter();
+
+	private Converter<Map<String, Object>, OAuth2AuthorizationServerMetadata> authorizationServerMetadataConverter = new OAuth2AuthorizationServerMetadataConverter();
+
+	private Converter<OAuth2AuthorizationServerMetadata, Map<String, Object>> authorizationServerMetadataParametersConverter = OAuth2AuthorizationServerMetadata::getClaims;
+
+	public OAuth2AuthorizationServerMetadataHttpMessageConverter() {
+		super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
+	}
+
+	@Override
+	protected boolean supports(Class<?> clazz) {
+		return OAuth2AuthorizationServerMetadata.class.isAssignableFrom(clazz);
+	}
+
+	@Override
+	@SuppressWarnings("unchecked")
+	protected OAuth2AuthorizationServerMetadata readInternal(Class<? extends OAuth2AuthorizationServerMetadata> clazz,
+			HttpInputMessage inputMessage) throws HttpMessageNotReadableException {
+		try {
+			Map<String, Object> authorizationServerMetadataParameters = (Map<String, Object>) this.jsonMessageConverter
+				.read(STRING_OBJECT_MAP.getType(), null, inputMessage);
+			return this.authorizationServerMetadataConverter.convert(authorizationServerMetadataParameters);
+		}
+		catch (Exception ex) {
+			throw new HttpMessageNotReadableException(
+					"An error occurred reading the OAuth 2.0 Authorization Server Metadata: " + ex.getMessage(), ex,
+					inputMessage);
+		}
+	}
+
+	@Override
+	protected void writeInternal(OAuth2AuthorizationServerMetadata authorizationServerMetadata,
+			HttpOutputMessage outputMessage) throws HttpMessageNotWritableException {
+		try {
+			Map<String, Object> authorizationServerMetadataResponseParameters = this.authorizationServerMetadataParametersConverter
+				.convert(authorizationServerMetadata);
+			this.jsonMessageConverter.write(authorizationServerMetadataResponseParameters, STRING_OBJECT_MAP.getType(),
+					MediaType.APPLICATION_JSON, outputMessage);
+		}
+		catch (Exception ex) {
+			throw new HttpMessageNotWritableException(
+					"An error occurred writing the OAuth 2.0 Authorization Server Metadata: " + ex.getMessage(), ex);
+		}
+	}
+
+	/**
+	 * Sets the {@link Converter} used for converting the OAuth 2.0 Authorization Server
+	 * Metadata parameters to an {@link OAuth2AuthorizationServerMetadata}.
+	 * @param authorizationServerMetadataConverter the {@link Converter} used for
+	 * converting to an {@link OAuth2AuthorizationServerMetadata}.
+	 */
+	public final void setAuthorizationServerMetadataConverter(
+			Converter<Map<String, Object>, OAuth2AuthorizationServerMetadata> authorizationServerMetadataConverter) {
+		Assert.notNull(authorizationServerMetadataConverter, "authorizationServerMetadataConverter cannot be null");
+		this.authorizationServerMetadataConverter = authorizationServerMetadataConverter;
+	}
+
+	/**
+	 * Sets the {@link Converter} used for converting the
+	 * {@link OAuth2AuthorizationServerMetadata} to a {@code Map} representation of the
+	 * OAuth 2.0 Authorization Server Metadata.
+	 * @param authorizationServerMetadataParametersConverter the {@link Converter} used
+	 * for converting to a {@code Map} representation of the OAuth 2.0 Authorization
+	 * Server Metadata.
+	 */
+	public final void setAuthorizationServerMetadataParametersConverter(
+			Converter<OAuth2AuthorizationServerMetadata, Map<String, Object>> authorizationServerMetadataParametersConverter) {
+		Assert.notNull(authorizationServerMetadataParametersConverter,
+				"authorizationServerMetadataParametersConverter cannot be null");
+		this.authorizationServerMetadataParametersConverter = authorizationServerMetadataParametersConverter;
+	}
+
+	private static final class OAuth2AuthorizationServerMetadataConverter
+			implements Converter<Map<String, Object>, OAuth2AuthorizationServerMetadata> {
+
+		private static final ClaimConversionService CLAIM_CONVERSION_SERVICE = ClaimConversionService
+			.getSharedInstance();
+
+		private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
+
+		private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
+
+		private static final TypeDescriptor URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class);
+
+		private final ClaimTypeConverter claimTypeConverter;
+
+		private OAuth2AuthorizationServerMetadataConverter() {
+			Converter<Object, ?> collectionStringConverter = getConverter(
+					TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR));
+			Converter<Object, ?> urlConverter = getConverter(URL_TYPE_DESCRIPTOR);
+
+			Map<String, Converter<Object, ?>> claimConverters = new HashMap<>();
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, urlConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, urlConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT,
+					urlConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT,
+					urlConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, urlConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED,
+					collectionStringConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, urlConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED,
+					collectionStringConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED,
+					collectionStringConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED,
+					collectionStringConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, urlConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED,
+					collectionStringConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT, urlConverter);
+			claimConverters.put(
+					OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED,
+					collectionStringConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED,
+					collectionStringConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.DPOP_SIGNING_ALG_VALUES_SUPPORTED,
+					collectionStringConverter);
+			this.claimTypeConverter = new ClaimTypeConverter(claimConverters);
+		}
+
+		@Override
+		public OAuth2AuthorizationServerMetadata convert(Map<String, Object> source) {
+			Map<String, Object> parsedClaims = this.claimTypeConverter.convert(source);
+			return OAuth2AuthorizationServerMetadata.withClaims(parsedClaims).build();
+		}
+
+		private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
+			return (source) -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor);
+		}
+
+	}
+
+}

+ 221 - 0
oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/http/converter/OAuth2TokenIntrospectionHttpMessageConverter.java

@@ -0,0 +1,221 @@
+/*
+ * Copyright 2020-2024 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.http.converter;
+
+import java.net.URL;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.core.convert.TypeDescriptor;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.HttpOutputMessage;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.AbstractHttpMessageConverter;
+import org.springframework.http.converter.GenericHttpMessageConverter;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.http.converter.HttpMessageNotWritableException;
+import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
+import org.springframework.security.oauth2.core.converter.ClaimConversionService;
+import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenIntrospection;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * A {@link HttpMessageConverter} for an {@link OAuth2TokenIntrospection OAuth 2.0 Token
+ * Introspection Response}.
+ *
+ * @author Gerardo Roza
+ * @author Joe Grandja
+ * @since 0.1.1
+ * @see AbstractHttpMessageConverter
+ * @see OAuth2TokenIntrospection
+ */
+public class OAuth2TokenIntrospectionHttpMessageConverter
+		extends AbstractHttpMessageConverter<OAuth2TokenIntrospection> {
+
+	private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() {
+	};
+
+	private final GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters
+		.getJsonMessageConverter();
+
+	private Converter<Map<String, Object>, OAuth2TokenIntrospection> tokenIntrospectionConverter = new MapOAuth2TokenIntrospectionConverter();
+
+	private Converter<OAuth2TokenIntrospection, Map<String, Object>> tokenIntrospectionParametersConverter = new OAuth2TokenIntrospectionMapConverter();
+
+	public OAuth2TokenIntrospectionHttpMessageConverter() {
+		super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
+	}
+
+	@Override
+	protected boolean supports(Class<?> clazz) {
+		return OAuth2TokenIntrospection.class.isAssignableFrom(clazz);
+	}
+
+	@Override
+	@SuppressWarnings("unchecked")
+	protected OAuth2TokenIntrospection readInternal(Class<? extends OAuth2TokenIntrospection> clazz,
+			HttpInputMessage inputMessage) throws HttpMessageNotReadableException {
+		try {
+			Map<String, Object> tokenIntrospectionParameters = (Map<String, Object>) this.jsonMessageConverter
+				.read(STRING_OBJECT_MAP.getType(), null, inputMessage);
+			return this.tokenIntrospectionConverter.convert(tokenIntrospectionParameters);
+		}
+		catch (Exception ex) {
+			throw new HttpMessageNotReadableException(
+					"An error occurred reading the Token Introspection Response: " + ex.getMessage(), ex, inputMessage);
+		}
+	}
+
+	@Override
+	protected void writeInternal(OAuth2TokenIntrospection tokenIntrospection, HttpOutputMessage outputMessage)
+			throws HttpMessageNotWritableException {
+		try {
+			Map<String, Object> tokenIntrospectionResponseParameters = this.tokenIntrospectionParametersConverter
+				.convert(tokenIntrospection);
+			this.jsonMessageConverter.write(tokenIntrospectionResponseParameters, STRING_OBJECT_MAP.getType(),
+					MediaType.APPLICATION_JSON, outputMessage);
+		}
+		catch (Exception ex) {
+			throw new HttpMessageNotWritableException(
+					"An error occurred writing the Token Introspection Response: " + ex.getMessage(), ex);
+		}
+	}
+
+	/**
+	 * Sets the {@link Converter} used for converting the Token Introspection Response
+	 * parameters to an {@link OAuth2TokenIntrospection}.
+	 * @param tokenIntrospectionConverter the {@link Converter} used for converting to an
+	 * {@link OAuth2TokenIntrospection}
+	 */
+	public final void setTokenIntrospectionConverter(
+			Converter<Map<String, Object>, OAuth2TokenIntrospection> tokenIntrospectionConverter) {
+		Assert.notNull(tokenIntrospectionConverter, "tokenIntrospectionConverter cannot be null");
+		this.tokenIntrospectionConverter = tokenIntrospectionConverter;
+	}
+
+	/**
+	 * Sets the {@link Converter} used for converting an {@link OAuth2TokenIntrospection}
+	 * to a {@code Map} representation of the Token Introspection Response parameters.
+	 * @param tokenIntrospectionParametersConverter the {@link Converter} used for
+	 * converting to a {@code Map} representation of the Token Introspection Response
+	 * parameters
+	 */
+	public final void setTokenIntrospectionParametersConverter(
+			Converter<OAuth2TokenIntrospection, Map<String, Object>> tokenIntrospectionParametersConverter) {
+		Assert.notNull(tokenIntrospectionParametersConverter, "tokenIntrospectionParametersConverter cannot be null");
+		this.tokenIntrospectionParametersConverter = tokenIntrospectionParametersConverter;
+	}
+
+	private static final class MapOAuth2TokenIntrospectionConverter
+			implements Converter<Map<String, Object>, OAuth2TokenIntrospection> {
+
+		private static final ClaimConversionService CLAIM_CONVERSION_SERVICE = ClaimConversionService
+			.getSharedInstance();
+
+		private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
+
+		private static final TypeDescriptor BOOLEAN_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Boolean.class);
+
+		private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
+
+		private static final TypeDescriptor INSTANT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Instant.class);
+
+		private static final TypeDescriptor URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class);
+
+		private final ClaimTypeConverter claimTypeConverter;
+
+		private MapOAuth2TokenIntrospectionConverter() {
+			Converter<Object, ?> booleanConverter = getConverter(BOOLEAN_TYPE_DESCRIPTOR);
+			Converter<Object, ?> stringConverter = getConverter(STRING_TYPE_DESCRIPTOR);
+			Converter<Object, ?> instantConverter = getConverter(INSTANT_TYPE_DESCRIPTOR);
+			Converter<Object, ?> collectionStringConverter = getConverter(
+					TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR));
+			Converter<Object, ?> urlConverter = getConverter(URL_TYPE_DESCRIPTOR);
+
+			Map<String, Converter<Object, ?>> claimConverters = new HashMap<>();
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.ACTIVE, booleanConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.SCOPE,
+					MapOAuth2TokenIntrospectionConverter::convertScope);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, stringConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.USERNAME, stringConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.TOKEN_TYPE, stringConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.EXP, instantConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.IAT, instantConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.NBF, instantConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.SUB, stringConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.AUD, collectionStringConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.ISS, urlConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.JTI, stringConverter);
+			this.claimTypeConverter = new ClaimTypeConverter(claimConverters);
+		}
+
+		@Override
+		public OAuth2TokenIntrospection convert(Map<String, Object> source) {
+			Map<String, Object> parsedClaims = this.claimTypeConverter.convert(source);
+			return OAuth2TokenIntrospection.withClaims(parsedClaims).build();
+		}
+
+		private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
+			return (source) -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor);
+		}
+
+		private static List<String> convertScope(Object scope) {
+			if (scope == null) {
+				return Collections.emptyList();
+			}
+			return Arrays.asList(StringUtils.delimitedListToStringArray(scope.toString(), " "));
+		}
+
+	}
+
+	private static final class OAuth2TokenIntrospectionMapConverter
+			implements Converter<OAuth2TokenIntrospection, Map<String, Object>> {
+
+		@Override
+		public Map<String, Object> convert(OAuth2TokenIntrospection source) {
+			Map<String, Object> responseClaims = new LinkedHashMap<>(source.getClaims());
+			if (!CollectionUtils.isEmpty(source.getScopes())) {
+				responseClaims.put(OAuth2TokenIntrospectionClaimNames.SCOPE,
+						StringUtils.collectionToDelimitedString(source.getScopes(), " "));
+			}
+			if (source.getExpiresAt() != null) {
+				responseClaims.put(OAuth2TokenIntrospectionClaimNames.EXP, source.getExpiresAt().getEpochSecond());
+			}
+			if (source.getIssuedAt() != null) {
+				responseClaims.put(OAuth2TokenIntrospectionClaimNames.IAT, source.getIssuedAt().getEpochSecond());
+			}
+			if (source.getNotBefore() != null) {
+				responseClaims.put(OAuth2TokenIntrospectionClaimNames.NBF, source.getNotBefore().getEpochSecond());
+			}
+			return responseClaims;
+		}
+
+	}
+
+}

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff