Add Mutual-TLS client certificate-bound access tokens
Issue gh-101 Closes gh-1560
This commit is contained in:
@@ -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 <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>
|
||||
*/
|
||||
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}.
|
||||
*
|
||||
|
||||
@@ -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 <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>
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 <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>
|
||||
*/
|
||||
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() {
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
|
||||
@@ -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}.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Jwt> {
|
||||
}
|
||||
}
|
||||
}
|
||||
claimsBuilder.claims(new DefaultOAuth2TokenClaimsConsumer(context));
|
||||
// @formatter:on
|
||||
|
||||
JwsHeader.Builder jwsHeaderBuilder = JwsHeader.with(jwsAlgorithm);
|
||||
|
||||
@@ -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<OA
|
||||
if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) {
|
||||
claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes());
|
||||
}
|
||||
claimsBuilder.claims(new DefaultOAuth2TokenClaimsConsumer(context));
|
||||
// @formatter:on
|
||||
|
||||
if (this.accessTokenCustomizer != null) {
|
||||
|
||||
@@ -106,7 +106,8 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP
|
||||
.tokenRevocationEndpointAuthenticationMethods(clientAuthenticationMethods())
|
||||
.tokenIntrospectionEndpoint(asUrl(issuer, authorizationServerSettings.getTokenIntrospectionEndpoint()))
|
||||
.tokenIntrospectionEndpointAuthenticationMethods(clientAuthenticationMethods())
|
||||
.codeChallengeMethod("S256");
|
||||
.codeChallengeMethod("S256")
|
||||
.tlsClientCertificateBoundAccessTokens(true);
|
||||
|
||||
this.authorizationServerMetadataCustomizer.accept(authorizationServerMetadata);
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -61,6 +61,7 @@ public class OAuth2AuthorizationServerMetadataTests {
|
||||
.tokenIntrospectionEndpoint("https://example.com/oauth2/introspect")
|
||||
.tokenIntrospectionEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
|
||||
.codeChallengeMethod("S256")
|
||||
.tlsClientCertificateBoundAccessTokens(true)
|
||||
.claim("a-claim", "a-value")
|
||||
.build();
|
||||
|
||||
@@ -77,6 +78,7 @@ public class OAuth2AuthorizationServerMetadataTests {
|
||||
assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isEqualTo(url("https://example.com/oauth2/introspect"));
|
||||
assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).containsExactly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
|
||||
assertThat(authorizationServerMetadata.getCodeChallengeMethods()).containsExactly("S256");
|
||||
assertThat(authorizationServerMetadata.isTlsClientCertificateBoundAccessTokens()).isTrue();
|
||||
assertThat(authorizationServerMetadata.getClaimAsString("a-claim")).isEqualTo("a-value");
|
||||
}
|
||||
|
||||
|
||||
@@ -131,6 +131,7 @@ public class OidcProviderConfigurationEndpointFilterTests {
|
||||
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("\"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("\"id_token_signing_alg_values_supported\":[\"RS256\"]");
|
||||
assertThat(providerConfigurationResponse).contains("\"userinfo_endpoint\":\"https://example.com/userinfo\"");
|
||||
|
||||
@@ -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,7 +34,7 @@ public class TokenSettingsTests {
|
||||
@Test
|
||||
public void buildWhenDefaultThenDefaultsAreSet() {
|
||||
TokenSettings tokenSettings = TokenSettings.builder().build();
|
||||
assertThat(tokenSettings.getSettings()).hasSize(7);
|
||||
assertThat(tokenSettings.getSettings()).hasSize(8);
|
||||
assertThat(tokenSettings.getAuthorizationCodeTimeToLive()).isEqualTo(Duration.ofMinutes(5));
|
||||
assertThat(tokenSettings.getAccessTokenTimeToLive()).isEqualTo(Duration.ofMinutes(5));
|
||||
assertThat(tokenSettings.getAccessTokenFormat()).isEqualTo(OAuth2TokenFormat.SELF_CONTAINED);
|
||||
@@ -42,6 +42,7 @@ public class TokenSettingsTests {
|
||||
assertThat(tokenSettings.isReuseRefreshTokens()).isTrue();
|
||||
assertThat(tokenSettings.getRefreshTokenTimeToLive()).isEqualTo(Duration.ofMinutes(60));
|
||||
assertThat(tokenSettings.getIdTokenSignatureAlgorithm()).isEqualTo(SignatureAlgorithm.RS256);
|
||||
assertThat(tokenSettings.isX509CertificateBoundAccessTokens()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -158,13 +159,21 @@ public class TokenSettingsTests {
|
||||
assertThat(tokenSettings.getIdTokenSignatureAlgorithm()).isEqualTo(idTokenSignatureAlgorithm);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void x509CertificateBoundAccessTokensWhenTrueThenSet() {
|
||||
TokenSettings tokenSettings = TokenSettings.builder()
|
||||
.x509CertificateBoundAccessTokens(true)
|
||||
.build();
|
||||
assertThat(tokenSettings.isX509CertificateBoundAccessTokens()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void settingWhenCustomThenSet() {
|
||||
TokenSettings tokenSettings = TokenSettings.builder()
|
||||
.setting("name1", "value1")
|
||||
.settings(settings -> settings.put("name2", "value2"))
|
||||
.build();
|
||||
assertThat(tokenSettings.getSettings()).hasSize(9);
|
||||
assertThat(tokenSettings.getSettings()).hasSize(10);
|
||||
assertThat(tokenSettings.<String>getSetting("name1")).isEqualTo("value1");
|
||||
assertThat(tokenSettings.<String>getSetting("name2")).isEqualTo("value2");
|
||||
}
|
||||
|
||||
@@ -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<JwtEncodingContext> 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<String> 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<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 {
|
||||
assertThat(jwtClaimsSet.<String>getClaim(IdTokenClaimNames.AZP)).isEqualTo(tokenContext.getRegisteredClient().getClientId());
|
||||
if (tokenContext.getAuthorizationGrantType().equals(AuthorizationGrantType.AUTHORIZATION_CODE)) {
|
||||
|
||||
@@ -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<OAuth2TokenClaimsContext> 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<String> scopes = accessTokenClaims.getClaim(OAuth2ParameterNames.SCOPE);
|
||||
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);
|
||||
verify(this.accessTokenCustomizer).customize(tokenClaimsContextCaptor.capture());
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user