diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java index 1c382949..677ce611 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java +++ b/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"); * 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. * * @author Daniel Garnier-Moiroux + * @author Joe Grandja * @see OAuth2AuthorizationServerMetadataClaimAccessor * @since 0.1.1 * @see 3.2. Authorization Server Metadata Response * @see 4.2. OpenID Provider Configuration Response * @see 4. Device Authorization Grant Metadata + * @see 3.3 Mutual-TLS Client Certificate-Bound Access Tokens Metadata */ public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth2AuthorizationServerMetadataClaimAccessor, Serializable { private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID; @@ -320,6 +322,17 @@ public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth 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}. * diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java index 089944d1..d2ce46aa 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java +++ b/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"); * 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. * * @author Daniel Garnier-Moiroux + * @author Joe Grandja * @since 0.1.1 * @see ClaimAccessor * @see OAuth2AuthorizationServerMetadataClaimNames * @see 2. Authorization Server Metadata * @see 3. OpenID Provider Metadata * @see 4. Device Authorization Grant Metadata + * @see 3.3 Mutual-TLS Client Certificate-Bound Access Tokens Metadata */ public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAccessor { @@ -171,4 +173,14 @@ public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAcc 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)); + } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java index 831e0ec7..f2decf90 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java +++ b/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"); * 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. * * @author Daniel Garnier-Moiroux + * @author Joe Grandja * @since 0.1.1 * @see 2. Authorization Server Metadata * @see 3. OpenID Provider Metadata * @see 4. Device Authorization Grant Metadata + * @see 3.3 Mutual-TLS Client Certificate-Bound Access Tokens Metadata */ public class OAuth2AuthorizationServerMetadataClaimNames { @@ -104,6 +106,12 @@ public class OAuth2AuthorizationServerMetadataClaimNames { */ 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() { } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java index 70639cee..41aa8373 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java @@ -111,6 +111,7 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques .tokenIntrospectionEndpoint(asUrl(issuer, authorizationServerSettings.getTokenIntrospectionEndpoint())) .tokenIntrospectionEndpointAuthenticationMethods(clientAuthenticationMethods()) .codeChallengeMethod("S256") + .tlsClientCertificateBoundAccessTokens(true) .subjectType("public") .idTokenSigningAlgorithm(SignatureAlgorithm.RS256.getName()) .scope(OidcScopes.OPENID); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java index 5b35f5b3..27f5c609 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java +++ b/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"); + /** + * 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() { } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/TokenSettings.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/TokenSettings.java index 2cbb024c..c222cb1f 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/TokenSettings.java +++ b/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"); * 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); } + /** + * 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. * @@ -116,7 +128,8 @@ public final class TokenSettings extends AbstractSettings { .deviceCodeTimeToLive(Duration.ofMinutes(5)) .reuseRefreshTokens(true) .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); } + /** + * 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}. * diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/DefaultOAuth2TokenClaimsConsumer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/DefaultOAuth2TokenClaimsConsumer.java new file mode 100644 index 00000000..98edcc2a --- /dev/null +++ b/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> { + 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 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 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); + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java index 84247b54..b57573b7 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java @@ -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"); * you may not use this file except in compliance with the License. @@ -144,6 +144,7 @@ public final class JwtGenerator implements OAuth2TokenGenerator { } } } + claimsBuilder.claims(new DefaultOAuth2TokenClaimsConsumer(context)); // @formatter:on JwsHeader.Builder jwsHeaderBuilder = JwsHeader.with(jwsAlgorithm); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/OAuth2AccessTokenGenerator.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/OAuth2AccessTokenGenerator.java index 4e203335..e3a9d1ee 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/OAuth2AccessTokenGenerator.java +++ b/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"); * you may not use this file except in compliance with the License. @@ -84,6 +84,7 @@ public final class OAuth2AccessTokenGenerator implements OAuth2TokenGenerator settings.put("name2", "value2")) .build(); - assertThat(tokenSettings.getSettings()).hasSize(9); + assertThat(tokenSettings.getSettings()).hasSize(10); assertThat(tokenSettings.getSetting("name1")).isEqualTo("value1"); assertThat(tokenSettings.getSetting("name2")).isEqualTo("value2"); } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java index 07deda57..063bf36f 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java @@ -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"); * 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.context.TestAuthorizationServerContext; 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.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.assertThatThrownBy; @@ -67,6 +69,8 @@ import static org.mockito.Mockito.verify; * @author Joe Grandja */ 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 JwtEncoder jwtEncoder; private OAuth2TokenCustomizer jwtCustomizer; @@ -128,11 +132,26 @@ public class JwtGeneratorTests { @Test 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(); 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.class.getName()); OAuth2AuthorizationCodeAuthenticationToken authentication = @@ -325,6 +344,17 @@ public class JwtGeneratorTests { Set scopes = jwtClaimsSet.getClaim(OAuth2ParameterNames.SCOPE); 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 cnf = jwtClaimsSet.getClaim("cnf"); + assertThat(cnf).isNotEmpty(); + assertThat(cnf.get("x5t#S256")).isNotNull(); + } else { + Map cnf = jwtClaimsSet.getClaim("cnf"); + assertThat(cnf).isEmpty(); + } } else { assertThat(jwtClaimsSet.getClaim(IdTokenClaimNames.AZP)).isEqualTo(tokenContext.getRegisteredClient().getClientId()); if (tokenContext.getAuthorizationGrantType().equals(AuthorizationGrantType.AUTHORIZATION_CODE)) { diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/OAuth2AccessTokenGeneratorTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/OAuth2AccessTokenGeneratorTests.java index 9b23496f..ef14c3a8 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/OAuth2AccessTokenGeneratorTests.java +++ b/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"); * 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.time.Instant; import java.util.Collections; +import java.util.Map; import java.util.Set; 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.TestAuthorizationServerContext; 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.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.assertThatThrownBy; @@ -55,6 +58,8 @@ import static org.mockito.Mockito.verify; * @author Joe Grandja */ public class OAuth2AccessTokenGeneratorTests { + private static final ClientAuthenticationMethod TLS_CLIENT_AUTH_AUTHENTICATION_METHOD = + new ClientAuthenticationMethod("tls_client_auth"); private OAuth2TokenCustomizer accessTokenCustomizer; private OAuth2AccessTokenGenerator accessTokenGenerator; private AuthorizationServerContext authorizationServerContext; @@ -114,10 +119,16 @@ public class OAuth2AccessTokenGeneratorTests { @Test public void generateWhenReferenceAccessTokenTypeThenReturnAccessToken() { // @formatter:off + ClientSettings clientSettings = ClientSettings.builder() + .x509CertificateSubjectDN(TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE[0].getSubjectX500Principal().getName()) + .build(); TokenSettings tokenSettings = TokenSettings.builder() .accessTokenFormat(OAuth2TokenFormat.REFERENCE) + .x509CertificateBoundAccessTokens(true) .build(); RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .clientAuthenticationMethod(TLS_CLIENT_AUTH_AUTHENTICATION_METHOD) + .clientSettings(clientSettings) .tokenSettings(tokenSettings) .build(); // @formatter:on @@ -125,7 +136,8 @@ public class OAuth2AccessTokenGeneratorTests { Authentication principal = authorization.getAttribute(Principal.class.getName()); 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.class.getName()); OAuth2AuthorizationCodeAuthenticationToken authentication = @@ -169,6 +181,10 @@ public class OAuth2AccessTokenGeneratorTests { Set scopes = accessTokenClaims.getClaim(OAuth2ParameterNames.SCOPE); assertThat(scopes).isEqualTo(tokenContext.getAuthorizedScopes()); + Map cnf = accessTokenClaims.getClaim("cnf"); + assertThat(cnf).isNotEmpty(); + assertThat(cnf.get("x5t#S256")).isNotNull(); + ArgumentCaptor tokenClaimsContextCaptor = ArgumentCaptor.forClass(OAuth2TokenClaimsContext.class); verify(this.accessTokenCustomizer).customize(tokenClaimsContextCaptor.capture()); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java index 8c2372c0..fbc2b12e 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java @@ -127,6 +127,7 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests { 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("\"code_challenge_methods_supported\":[\"S256\"]"); + assertThat(authorizationServerMetadataResponse).contains("\"tls_client_certificate_bound_access_tokens\":true"); } @Test