Przeglądaj źródła

Add Mutual-TLS client certificate-bound access tokens

Issue gh-101

Closes gh-1560
Joe Grandja 1 rok temu
rodzic
commit
b9b0bb751e
16 zmienionych plików z 224 dodań i 17 usunięć
  1. 14 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java
  2. 13 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java
  3. 9 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java
  4. 1 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java
  5. 7 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java
  6. 27 2
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/TokenSettings.java
  7. 79 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/DefaultOAuth2TokenClaimsConsumer.java
  8. 2 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java
  9. 2 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/OAuth2AccessTokenGenerator.java
  10. 2 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java
  11. 3 1
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java
  12. 1 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java
  13. 12 3
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/settings/TokenSettingsTests.java
  14. 33 3
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java
  15. 18 2
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/OAuth2AccessTokenGeneratorTests.java
  16. 1 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java

+ 14 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -34,11 +34,13 @@ import org.springframework.util.Assert;
  * The metadata endpoint returns a set of claims an Authorization Server describes about its configuration.
  * The metadata endpoint returns a set of claims an Authorization Server describes about its configuration.
  *
  *
  * @author Daniel Garnier-Moiroux
  * @author Daniel Garnier-Moiroux
+ * @author Joe Grandja
  * @see OAuth2AuthorizationServerMetadataClaimAccessor
  * @see OAuth2AuthorizationServerMetadataClaimAccessor
  * @since 0.1.1
  * @since 0.1.1
  * @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://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://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://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>
  */
  */
 public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth2AuthorizationServerMetadataClaimAccessor, Serializable {
 public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth2AuthorizationServerMetadataClaimAccessor, Serializable {
 	private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
 	private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
@@ -320,6 +322,17 @@ public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth
 			return getThis();
 			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);
+		}
+
 		/**
 		/**
 		 * Use this claim in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}.
 		 * Use this claim in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}.
 		 *
 		 *

+ 13 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -25,12 +25,14 @@ import org.springframework.security.oauth2.core.ClaimAccessor;
  * used in OAuth 2.0 Authorization Server Metadata and OpenID Connect Discovery 1.0.
  * used in OAuth 2.0 Authorization Server Metadata and OpenID Connect Discovery 1.0.
  *
  *
  * @author Daniel Garnier-Moiroux
  * @author Daniel Garnier-Moiroux
+ * @author Joe Grandja
  * @since 0.1.1
  * @since 0.1.1
  * @see ClaimAccessor
  * @see ClaimAccessor
  * @see OAuth2AuthorizationServerMetadataClaimNames
  * @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://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://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://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>
  */
  */
 public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAccessor {
 public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAccessor {
 
 
@@ -171,4 +173,14 @@ public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAcc
 		return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED);
 		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));
+	}
+
 }
 }

+ 9 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -20,10 +20,12 @@ package org.springframework.security.oauth2.server.authorization;
  * used in OAuth 2.0 Authorization Server Metadata and OpenID Connect Discovery 1.0.
  * used in OAuth 2.0 Authorization Server Metadata and OpenID Connect Discovery 1.0.
  *
  *
  * @author Daniel Garnier-Moiroux
  * @author Daniel Garnier-Moiroux
+ * @author Joe Grandja
  * @since 0.1.1
  * @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://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://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://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>
  */
  */
 public class OAuth2AuthorizationServerMetadataClaimNames {
 public class OAuth2AuthorizationServerMetadataClaimNames {
 
 
@@ -104,6 +106,12 @@ public class OAuth2AuthorizationServerMetadataClaimNames {
 	 */
 	 */
 	public static final String CODE_CHALLENGE_METHODS_SUPPORTED = "code_challenge_methods_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";
+
 	protected OAuth2AuthorizationServerMetadataClaimNames() {
 	protected OAuth2AuthorizationServerMetadataClaimNames() {
 	}
 	}
 
 

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

@@ -111,6 +111,7 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques
 				.tokenIntrospectionEndpoint(asUrl(issuer, authorizationServerSettings.getTokenIntrospectionEndpoint()))
 				.tokenIntrospectionEndpoint(asUrl(issuer, authorizationServerSettings.getTokenIntrospectionEndpoint()))
 				.tokenIntrospectionEndpointAuthenticationMethods(clientAuthenticationMethods())
 				.tokenIntrospectionEndpointAuthenticationMethods(clientAuthenticationMethods())
 				.codeChallengeMethod("S256")
 				.codeChallengeMethod("S256")
+				.tlsClientCertificateBoundAccessTokens(true)
 				.subjectType("public")
 				.subjectType("public")
 				.idTokenSigningAlgorithm(SignatureAlgorithm.RS256.getName())
 				.idTokenSigningAlgorithm(SignatureAlgorithm.RS256.getName())
 				.scope(OidcScopes.OPENID);
 				.scope(OidcScopes.OPENID);

+ 7 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java

@@ -189,6 +189,13 @@ public final class ConfigurationSettingNames {
 		 */
 		 */
 		public static final String ID_TOKEN_SIGNATURE_ALGORITHM = TOKEN_SETTINGS_NAMESPACE.concat("id-token-signature-algorithm");
 		public static final String ID_TOKEN_SIGNATURE_ALGORITHM = TOKEN_SETTINGS_NAMESPACE.concat("id-token-signature-algorithm");
 
 
+		/**
+		 * Set to {@code true} if access tokens must be bound to the client {@code X509Certificate}
+		 * received during client authentication when using the {@code tls_client_auth} or {@code self_signed_tls_client_auth} method.
+		 * @since 1.3
+		 */
+		public static final String X509_CERTIFICATE_BOUND_ACCESS_TOKENS = TOKEN_SETTINGS_NAMESPACE.concat("x509-certificate-bound-access-tokens");
+
 		private Token() {
 		private Token() {
 		}
 		}
 
 

+ 27 - 2
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/TokenSettings.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -103,6 +103,18 @@ public final class TokenSettings extends AbstractSettings {
 		return getSetting(ConfigurationSettingNames.Token.ID_TOKEN_SIGNATURE_ALGORITHM);
 		return getSetting(ConfigurationSettingNames.Token.ID_TOKEN_SIGNATURE_ALGORITHM);
 	}
 	}
 
 
+	/**
+	 * Returns {@code true} if access tokens must be bound to the client {@code X509Certificate}
+	 * received during client authentication when using the {@code tls_client_auth} or {@code self_signed_tls_client_auth} method.
+	 * The default is {@code false}.
+	 *
+	 * @return {@code true} if access tokens must be bound to the client {@code X509Certificate}, {@code false} otherwise
+	 * @since 1.3
+	 */
+	public boolean isX509CertificateBoundAccessTokens() {
+		return getSetting(ConfigurationSettingNames.Token.X509_CERTIFICATE_BOUND_ACCESS_TOKENS);
+	}
+
 	/**
 	/**
 	 * Constructs a new {@link Builder} with the default settings.
 	 * Constructs a new {@link Builder} with the default settings.
 	 *
 	 *
@@ -116,7 +128,8 @@ public final class TokenSettings extends AbstractSettings {
 				.deviceCodeTimeToLive(Duration.ofMinutes(5))
 				.deviceCodeTimeToLive(Duration.ofMinutes(5))
 				.reuseRefreshTokens(true)
 				.reuseRefreshTokens(true)
 				.refreshTokenTimeToLive(Duration.ofMinutes(60))
 				.refreshTokenTimeToLive(Duration.ofMinutes(60))
-				.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256);
+				.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
+				.x509CertificateBoundAccessTokens(false);
 	}
 	}
 
 
 	/**
 	/**
@@ -224,6 +237,18 @@ public final class TokenSettings extends AbstractSettings {
 			return setting(ConfigurationSettingNames.Token.ID_TOKEN_SIGNATURE_ALGORITHM, idTokenSignatureAlgorithm);
 			return setting(ConfigurationSettingNames.Token.ID_TOKEN_SIGNATURE_ALGORITHM, idTokenSignatureAlgorithm);
 		}
 		}
 
 
+		/**
+		 * Set to {@code true} if access tokens must be bound to the client {@code X509Certificate}
+		 * received during client authentication when using the {@code tls_client_auth} or {@code self_signed_tls_client_auth} method.
+		 *
+		 * @param x509CertificateBoundAccessTokens {@code true} if access tokens must be bound to the client {@code X509Certificate}, {@code false} otherwise
+		 * @return the {@link Builder} for further configuration
+		 * @since 1.3
+		 */
+		public Builder x509CertificateBoundAccessTokens(boolean x509CertificateBoundAccessTokens) {
+			return setting(ConfigurationSettingNames.Token.X509_CERTIFICATE_BOUND_ACCESS_TOKENS, x509CertificateBoundAccessTokens);
+		}
+
 		/**
 		/**
 		 * Builds the {@link TokenSettings}.
 		 * Builds the {@link TokenSettings}.
 		 *
 		 *

+ 79 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/DefaultOAuth2TokenClaimsConsumer.java

@@ -0,0 +1,79 @@
+/*
+ * 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.token;
+
+import java.security.MessageDigest;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
+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.server.authorization.OAuth2TokenType;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
+
+/**
+ * @author Joe Grandja
+ * @since 1.3
+ */
+final class DefaultOAuth2TokenClaimsConsumer implements Consumer<Map<String, Object>> {
+	private static final ClientAuthenticationMethod TLS_CLIENT_AUTH_AUTHENTICATION_METHOD =
+			new ClientAuthenticationMethod("tls_client_auth");
+	private static final ClientAuthenticationMethod SELF_SIGNED_TLS_CLIENT_AUTH_AUTHENTICATION_METHOD =
+			new ClientAuthenticationMethod("self_signed_tls_client_auth");
+	private final OAuth2TokenContext context;
+
+	DefaultOAuth2TokenClaimsConsumer(OAuth2TokenContext context) {
+		this.context = context;
+	}
+
+	@Override
+	public void accept(Map<String, Object> claims) {
+		// Add 'cnf' claim for Mutual-TLS Client Certificate-Bound Access Tokens
+		if (OAuth2TokenType.ACCESS_TOKEN.equals(this.context.getTokenType()) &&
+				this.context.getAuthorizationGrant() != null &&
+				this.context.getAuthorizationGrant().getPrincipal() instanceof OAuth2ClientAuthenticationToken clientAuthentication) {
+
+			if ((TLS_CLIENT_AUTH_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod()) ||
+					SELF_SIGNED_TLS_CLIENT_AUTH_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod())) &&
+					this.context.getRegisteredClient().getTokenSettings().isX509CertificateBoundAccessTokens()) {
+
+				X509Certificate[] clientCertificateChain = (X509Certificate[]) clientAuthentication.getCredentials();
+				try {
+					String sha256Thumbprint = computeSHA256Thumbprint(clientCertificateChain[0]);
+					Map<String, Object> x5tClaim = new HashMap<>();
+					x5tClaim.put("x5t#S256", sha256Thumbprint);
+					claims.put("cnf", x5tClaim);
+				} 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);
+				}
+			}
+		}
+	}
+
+	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);
+	}
+
+}

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -144,6 +144,7 @@ public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
 				}
 				}
 			}
 			}
 		}
 		}
+		claimsBuilder.claims(new DefaultOAuth2TokenClaimsConsumer(context));
 		// @formatter:on
 		// @formatter:on
 
 
 		JwsHeader.Builder jwsHeaderBuilder = JwsHeader.with(jwsAlgorithm);
 		JwsHeader.Builder jwsHeaderBuilder = JwsHeader.with(jwsAlgorithm);

+ 2 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/OAuth2AccessTokenGenerator.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -84,6 +84,7 @@ public final class OAuth2AccessTokenGenerator implements OAuth2TokenGenerator<OA
 		if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) {
 		if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) {
 			claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes());
 			claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes());
 		}
 		}
+		claimsBuilder.claims(new DefaultOAuth2TokenClaimsConsumer(context));
 		// @formatter:on
 		// @formatter:on
 
 
 		if (this.accessTokenCustomizer != null) {
 		if (this.accessTokenCustomizer != null) {

+ 2 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java

@@ -106,7 +106,8 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP
 				.tokenRevocationEndpointAuthenticationMethods(clientAuthenticationMethods())
 				.tokenRevocationEndpointAuthenticationMethods(clientAuthenticationMethods())
 				.tokenIntrospectionEndpoint(asUrl(issuer, authorizationServerSettings.getTokenIntrospectionEndpoint()))
 				.tokenIntrospectionEndpoint(asUrl(issuer, authorizationServerSettings.getTokenIntrospectionEndpoint()))
 				.tokenIntrospectionEndpointAuthenticationMethods(clientAuthenticationMethods())
 				.tokenIntrospectionEndpointAuthenticationMethods(clientAuthenticationMethods())
-				.codeChallengeMethod("S256");
+				.codeChallengeMethod("S256")
+				.tlsClientCertificateBoundAccessTokens(true);
 
 
 		this.authorizationServerMetadataCustomizer.accept(authorizationServerMetadata);
 		this.authorizationServerMetadataCustomizer.accept(authorizationServerMetadata);
 
 

+ 3 - 1
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -61,6 +61,7 @@ public class OAuth2AuthorizationServerMetadataTests {
 				.tokenIntrospectionEndpoint("https://example.com/oauth2/introspect")
 				.tokenIntrospectionEndpoint("https://example.com/oauth2/introspect")
 				.tokenIntrospectionEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
 				.tokenIntrospectionEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
 				.codeChallengeMethod("S256")
 				.codeChallengeMethod("S256")
+				.tlsClientCertificateBoundAccessTokens(true)
 				.claim("a-claim", "a-value")
 				.claim("a-claim", "a-value")
 				.build();
 				.build();
 
 
@@ -77,6 +78,7 @@ public class OAuth2AuthorizationServerMetadataTests {
 		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isEqualTo(url("https://example.com/oauth2/introspect"));
 		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isEqualTo(url("https://example.com/oauth2/introspect"));
 		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).containsExactly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
 		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).containsExactly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
 		assertThat(authorizationServerMetadata.getCodeChallengeMethods()).containsExactly("S256");
 		assertThat(authorizationServerMetadata.getCodeChallengeMethods()).containsExactly("S256");
+		assertThat(authorizationServerMetadata.isTlsClientCertificateBoundAccessTokens()).isTrue();
 		assertThat(authorizationServerMetadata.getClaimAsString("a-claim")).isEqualTo("a-value");
 		assertThat(authorizationServerMetadata.getClaimAsString("a-claim")).isEqualTo("a-value");
 	}
 	}
 
 

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

@@ -131,6 +131,7 @@ public class OidcProviderConfigurationEndpointFilterTests {
 		assertThat(providerConfigurationResponse).contains("\"introspection_endpoint\":\"https://example.com/oauth2/v1/introspect\"");
 		assertThat(providerConfigurationResponse).contains("\"introspection_endpoint\":\"https://example.com/oauth2/v1/introspect\"");
 		assertThat(providerConfigurationResponse).contains("\"introspection_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"tls_client_auth\",\"self_signed_tls_client_auth\"]");
 		assertThat(providerConfigurationResponse).contains("\"introspection_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"tls_client_auth\",\"self_signed_tls_client_auth\"]");
 		assertThat(providerConfigurationResponse).contains("\"code_challenge_methods_supported\":[\"S256\"]");
 		assertThat(providerConfigurationResponse).contains("\"code_challenge_methods_supported\":[\"S256\"]");
+		assertThat(providerConfigurationResponse).contains("\"tls_client_certificate_bound_access_tokens\":true");
 		assertThat(providerConfigurationResponse).contains("\"subject_types_supported\":[\"public\"]");
 		assertThat(providerConfigurationResponse).contains("\"subject_types_supported\":[\"public\"]");
 		assertThat(providerConfigurationResponse).contains("\"id_token_signing_alg_values_supported\":[\"RS256\"]");
 		assertThat(providerConfigurationResponse).contains("\"id_token_signing_alg_values_supported\":[\"RS256\"]");
 		assertThat(providerConfigurationResponse).contains("\"userinfo_endpoint\":\"https://example.com/userinfo\"");
 		assertThat(providerConfigurationResponse).contains("\"userinfo_endpoint\":\"https://example.com/userinfo\"");

+ 12 - 3
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/settings/TokenSettingsTests.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -34,7 +34,7 @@ public class TokenSettingsTests {
 	@Test
 	@Test
 	public void buildWhenDefaultThenDefaultsAreSet() {
 	public void buildWhenDefaultThenDefaultsAreSet() {
 		TokenSettings tokenSettings = TokenSettings.builder().build();
 		TokenSettings tokenSettings = TokenSettings.builder().build();
-		assertThat(tokenSettings.getSettings()).hasSize(7);
+		assertThat(tokenSettings.getSettings()).hasSize(8);
 		assertThat(tokenSettings.getAuthorizationCodeTimeToLive()).isEqualTo(Duration.ofMinutes(5));
 		assertThat(tokenSettings.getAuthorizationCodeTimeToLive()).isEqualTo(Duration.ofMinutes(5));
 		assertThat(tokenSettings.getAccessTokenTimeToLive()).isEqualTo(Duration.ofMinutes(5));
 		assertThat(tokenSettings.getAccessTokenTimeToLive()).isEqualTo(Duration.ofMinutes(5));
 		assertThat(tokenSettings.getAccessTokenFormat()).isEqualTo(OAuth2TokenFormat.SELF_CONTAINED);
 		assertThat(tokenSettings.getAccessTokenFormat()).isEqualTo(OAuth2TokenFormat.SELF_CONTAINED);
@@ -42,6 +42,7 @@ public class TokenSettingsTests {
 		assertThat(tokenSettings.isReuseRefreshTokens()).isTrue();
 		assertThat(tokenSettings.isReuseRefreshTokens()).isTrue();
 		assertThat(tokenSettings.getRefreshTokenTimeToLive()).isEqualTo(Duration.ofMinutes(60));
 		assertThat(tokenSettings.getRefreshTokenTimeToLive()).isEqualTo(Duration.ofMinutes(60));
 		assertThat(tokenSettings.getIdTokenSignatureAlgorithm()).isEqualTo(SignatureAlgorithm.RS256);
 		assertThat(tokenSettings.getIdTokenSignatureAlgorithm()).isEqualTo(SignatureAlgorithm.RS256);
+		assertThat(tokenSettings.isX509CertificateBoundAccessTokens()).isFalse();
 	}
 	}
 
 
 	@Test
 	@Test
@@ -158,13 +159,21 @@ public class TokenSettingsTests {
 		assertThat(tokenSettings.getIdTokenSignatureAlgorithm()).isEqualTo(idTokenSignatureAlgorithm);
 		assertThat(tokenSettings.getIdTokenSignatureAlgorithm()).isEqualTo(idTokenSignatureAlgorithm);
 	}
 	}
 
 
+	@Test
+	public void x509CertificateBoundAccessTokensWhenTrueThenSet() {
+		TokenSettings tokenSettings = TokenSettings.builder()
+				.x509CertificateBoundAccessTokens(true)
+				.build();
+		assertThat(tokenSettings.isX509CertificateBoundAccessTokens()).isTrue();
+	}
+
 	@Test
 	@Test
 	public void settingWhenCustomThenSet() {
 	public void settingWhenCustomThenSet() {
 		TokenSettings tokenSettings = TokenSettings.builder()
 		TokenSettings tokenSettings = TokenSettings.builder()
 				.setting("name1", "value1")
 				.setting("name1", "value1")
 				.settings(settings -> settings.put("name2", "value2"))
 				.settings(settings -> settings.put("name2", "value2"))
 				.build();
 				.build();
-		assertThat(tokenSettings.getSettings()).hasSize(9);
+		assertThat(tokenSettings.getSettings()).hasSize(10);
 		assertThat(tokenSettings.<String>getSetting("name1")).isEqualTo("value1");
 		assertThat(tokenSettings.<String>getSetting("name1")).isEqualTo("value1");
 		assertThat(tokenSettings.<String>getSetting("name2")).isEqualTo("value2");
 		assertThat(tokenSettings.<String>getSetting("name2")).isEqualTo("value2");
 	}
 	}

+ 33 - 3
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -53,8 +53,10 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
 import org.springframework.security.oauth2.server.authorization.context.TestAuthorizationServerContext;
 import org.springframework.security.oauth2.server.authorization.context.TestAuthorizationServerContext;
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
 import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
 import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
 import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
 import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
+import org.springframework.security.oauth2.server.authorization.util.TestX509Certificates;
 
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -67,6 +69,8 @@ import static org.mockito.Mockito.verify;
  * @author Joe Grandja
  * @author Joe Grandja
  */
  */
 public class JwtGeneratorTests {
 public class JwtGeneratorTests {
+	private static final ClientAuthenticationMethod TLS_CLIENT_AUTH_AUTHENTICATION_METHOD =
+			new ClientAuthenticationMethod("tls_client_auth");
 	private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
 	private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
 	private JwtEncoder jwtEncoder;
 	private JwtEncoder jwtEncoder;
 	private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer;
 	private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer;
@@ -128,11 +132,26 @@ public class JwtGeneratorTests {
 
 
 	@Test
 	@Test
 	public void generateWhenAccessTokenTypeThenReturnJwt() {
 	public void generateWhenAccessTokenTypeThenReturnJwt() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		// @formatter:off
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.clientAuthenticationMethod(TLS_CLIENT_AUTH_AUTHENTICATION_METHOD)
+				.clientSettings(
+						ClientSettings.builder()
+								.x509CertificateSubjectDN(TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE[0].getSubjectX500Principal().getName())
+								.build()
+				)
+				.tokenSettings(
+						TokenSettings.builder()
+								.x509CertificateBoundAccessTokens(true)
+								.build()
+				)
+				.build();
+		// @formatter:on
 		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
 		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
 
 
 		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
 		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
-				registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+				registeredClient, TLS_CLIENT_AUTH_AUTHENTICATION_METHOD,
+				TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE);
 		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
 		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
 				OAuth2AuthorizationRequest.class.getName());
 				OAuth2AuthorizationRequest.class.getName());
 		OAuth2AuthorizationCodeAuthenticationToken authentication =
 		OAuth2AuthorizationCodeAuthenticationToken authentication =
@@ -325,6 +344,17 @@ public class JwtGeneratorTests {
 
 
 			Set<String> scopes = jwtClaimsSet.getClaim(OAuth2ParameterNames.SCOPE);
 			Set<String> scopes = jwtClaimsSet.getClaim(OAuth2ParameterNames.SCOPE);
 			assertThat(scopes).isEqualTo(tokenContext.getAuthorizedScopes());
 			assertThat(scopes).isEqualTo(tokenContext.getAuthorizedScopes());
+
+			OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) tokenContext.getAuthorizationGrant().getPrincipal();
+			if (TLS_CLIENT_AUTH_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod()) &&
+					tokenContext.getRegisteredClient().getTokenSettings().isX509CertificateBoundAccessTokens()) {
+				Map<String, Object> cnf = jwtClaimsSet.getClaim("cnf");
+				assertThat(cnf).isNotEmpty();
+				assertThat(cnf.get("x5t#S256")).isNotNull();
+			} else {
+				Map<String, Object> cnf = jwtClaimsSet.getClaim("cnf");
+				assertThat(cnf).isEmpty();
+			}
 		} else {
 		} else {
 			assertThat(jwtClaimsSet.<String>getClaim(IdTokenClaimNames.AZP)).isEqualTo(tokenContext.getRegisteredClient().getClientId());
 			assertThat(jwtClaimsSet.<String>getClaim(IdTokenClaimNames.AZP)).isEqualTo(tokenContext.getRegisteredClient().getClientId());
 			if (tokenContext.getAuthorizationGrantType().equals(AuthorizationGrantType.AUTHORIZATION_CODE)) {
 			if (tokenContext.getAuthorizationGrantType().equals(AuthorizationGrantType.AUTHORIZATION_CODE)) {

+ 18 - 2
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/OAuth2AccessTokenGeneratorTests.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.security.oauth2.server.authorization.token;
 import java.security.Principal;
 import java.security.Principal;
 import java.time.Instant;
 import java.time.Instant;
 import java.util.Collections;
 import java.util.Collections;
+import java.util.Map;
 import java.util.Set;
 import java.util.Set;
 
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.BeforeEach;
@@ -41,8 +42,10 @@ import org.springframework.security.oauth2.server.authorization.client.TestRegis
 import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
 import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
 import org.springframework.security.oauth2.server.authorization.context.TestAuthorizationServerContext;
 import org.springframework.security.oauth2.server.authorization.context.TestAuthorizationServerContext;
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
 import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
 import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
 import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
 import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
+import org.springframework.security.oauth2.server.authorization.util.TestX509Certificates;
 
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -55,6 +58,8 @@ import static org.mockito.Mockito.verify;
  * @author Joe Grandja
  * @author Joe Grandja
  */
  */
 public class OAuth2AccessTokenGeneratorTests {
 public class OAuth2AccessTokenGeneratorTests {
+	private static final ClientAuthenticationMethod TLS_CLIENT_AUTH_AUTHENTICATION_METHOD =
+			new ClientAuthenticationMethod("tls_client_auth");
 	private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;
 	private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;
 	private OAuth2AccessTokenGenerator accessTokenGenerator;
 	private OAuth2AccessTokenGenerator accessTokenGenerator;
 	private AuthorizationServerContext authorizationServerContext;
 	private AuthorizationServerContext authorizationServerContext;
@@ -114,10 +119,16 @@ public class OAuth2AccessTokenGeneratorTests {
 	@Test
 	@Test
 	public void generateWhenReferenceAccessTokenTypeThenReturnAccessToken() {
 	public void generateWhenReferenceAccessTokenTypeThenReturnAccessToken() {
 		// @formatter:off
 		// @formatter:off
+		ClientSettings clientSettings = ClientSettings.builder()
+				.x509CertificateSubjectDN(TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE[0].getSubjectX500Principal().getName())
+				.build();
 		TokenSettings tokenSettings = TokenSettings.builder()
 		TokenSettings tokenSettings = TokenSettings.builder()
 				.accessTokenFormat(OAuth2TokenFormat.REFERENCE)
 				.accessTokenFormat(OAuth2TokenFormat.REFERENCE)
+				.x509CertificateBoundAccessTokens(true)
 				.build();
 				.build();
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.clientAuthenticationMethod(TLS_CLIENT_AUTH_AUTHENTICATION_METHOD)
+				.clientSettings(clientSettings)
 				.tokenSettings(tokenSettings)
 				.tokenSettings(tokenSettings)
 				.build();
 				.build();
 		// @formatter:on
 		// @formatter:on
@@ -125,7 +136,8 @@ public class OAuth2AccessTokenGeneratorTests {
 		Authentication principal = authorization.getAttribute(Principal.class.getName());
 		Authentication principal = authorization.getAttribute(Principal.class.getName());
 
 
 		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
 		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
-				registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+				registeredClient, TLS_CLIENT_AUTH_AUTHENTICATION_METHOD,
+				TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE);
 		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
 		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
 				OAuth2AuthorizationRequest.class.getName());
 				OAuth2AuthorizationRequest.class.getName());
 		OAuth2AuthorizationCodeAuthenticationToken authentication =
 		OAuth2AuthorizationCodeAuthenticationToken authentication =
@@ -169,6 +181,10 @@ public class OAuth2AccessTokenGeneratorTests {
 		Set<String> scopes = accessTokenClaims.getClaim(OAuth2ParameterNames.SCOPE);
 		Set<String> scopes = accessTokenClaims.getClaim(OAuth2ParameterNames.SCOPE);
 		assertThat(scopes).isEqualTo(tokenContext.getAuthorizedScopes());
 		assertThat(scopes).isEqualTo(tokenContext.getAuthorizedScopes());
 
 
+		Map<String, Object> cnf = accessTokenClaims.getClaim("cnf");
+		assertThat(cnf).isNotEmpty();
+		assertThat(cnf.get("x5t#S256")).isNotNull();
+
 		ArgumentCaptor<OAuth2TokenClaimsContext> tokenClaimsContextCaptor = ArgumentCaptor.forClass(OAuth2TokenClaimsContext.class);
 		ArgumentCaptor<OAuth2TokenClaimsContext> tokenClaimsContextCaptor = ArgumentCaptor.forClass(OAuth2TokenClaimsContext.class);
 		verify(this.accessTokenCustomizer).customize(tokenClaimsContextCaptor.capture());
 		verify(this.accessTokenCustomizer).customize(tokenClaimsContextCaptor.capture());
 
 

+ 1 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java

@@ -127,6 +127,7 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests {
 		assertThat(authorizationServerMetadataResponse).contains("\"introspection_endpoint\":\"https://example.com/oauth2/v1/introspect\"");
 		assertThat(authorizationServerMetadataResponse).contains("\"introspection_endpoint\":\"https://example.com/oauth2/v1/introspect\"");
 		assertThat(authorizationServerMetadataResponse).contains("\"introspection_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"tls_client_auth\",\"self_signed_tls_client_auth\"]");
 		assertThat(authorizationServerMetadataResponse).contains("\"introspection_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"tls_client_auth\",\"self_signed_tls_client_auth\"]");
 		assertThat(authorizationServerMetadataResponse).contains("\"code_challenge_methods_supported\":[\"S256\"]");
 		assertThat(authorizationServerMetadataResponse).contains("\"code_challenge_methods_supported\":[\"S256\"]");
+		assertThat(authorizationServerMetadataResponse).contains("\"tls_client_certificate_bound_access_tokens\":true");
 	}
 	}
 
 
 	@Test
 	@Test