Browse Source

Add Mutual-TLS client certificate-bound access tokens

Issue gh-101

Closes gh-1560
pull/1609/head
Joe Grandja 2 years ago
parent
commit
b9b0bb751e
  1. 15
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java
  2. 14
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java
  3. 10
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java
  4. 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java
  5. 7
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java
  6. 29
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/TokenSettings.java
  7. 79
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/DefaultOAuth2TokenClaimsConsumer.java
  8. 3
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java
  9. 3
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/OAuth2AccessTokenGenerator.java
  10. 3
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java
  11. 4
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java
  12. 1
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java
  13. 15
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/settings/TokenSettingsTests.java
  14. 36
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java
  15. 20
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/OAuth2AccessTokenGeneratorTests.java
  16. 1
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java

15
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java

@ -1,5 +1,5 @@ @@ -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; @@ -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 @@ -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}.
*

14
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java

@ -1,5 +1,5 @@ @@ -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; @@ -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 @@ -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));
}
}

10
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java

@ -1,5 +1,5 @@ @@ -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; @@ -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 { @@ -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() {
}

1
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 @@ -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);

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

@ -189,6 +189,13 @@ public final class ConfigurationSettingNames { @@ -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() {
}

29
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/TokenSettings.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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}.
*

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

@ -0,0 +1,79 @@ @@ -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);
}
}

3
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java

@ -1,5 +1,5 @@ @@ -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> { @@ -144,6 +144,7 @@ public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
}
}
}
claimsBuilder.claims(new DefaultOAuth2TokenClaimsConsumer(context));
// @formatter:on
JwsHeader.Builder jwsHeaderBuilder = JwsHeader.with(jwsAlgorithm);

3
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/OAuth2AccessTokenGenerator.java

@ -1,5 +1,5 @@ @@ -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 @@ -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) {

3
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 @@ -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);

4
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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 { @@ -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");
}

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

@ -131,6 +131,7 @@ public class OidcProviderConfigurationEndpointFilterTests { @@ -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\"");

15
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/settings/TokenSettingsTests.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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");
}

36
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java

@ -1,5 +1,5 @@ @@ -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 @@ -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; @@ -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 { @@ -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 { @@ -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)) {

20
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/OAuth2AccessTokenGeneratorTests.java

@ -1,5 +1,5 @@ @@ -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; @@ -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 @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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());

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

@ -127,6 +127,7 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests { @@ -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

Loading…
Cancel
Save