diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimAccessor.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimAccessor.java index 161ec9fd..83ebe182 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimAccessor.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimAccessor.java @@ -20,7 +20,10 @@ import java.time.Instant; import java.util.List; import org.springframework.security.oauth2.core.ClaimAccessor; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.Jwt; /** * A {@link ClaimAccessor} for the "claims" that are contained @@ -100,14 +103,12 @@ public interface OidcClientMetadataClaimAccessor extends ClaimAccessor { } /** - * Returns the {@link SignatureAlgorithm JWS} algorithm that must be used for signing the JWT used to authenticate - * the Client at the Token Endpoint for the {@code private_key_jwt} and {@code client_secret_jwt} authentication - * methods {@code (token_endpoint_auth_signing_alg)} + * Returns the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate + * the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and + * {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods {@code (token_endpoint_auth_signing_alg)}. * - * @return the {@link SignatureAlgorithm JWS} algorithm that must be used for signing the JWT used to authenticate - * the Client at the Token Endpoint for the {@code private_key_jwt} and {@code client_secret_jwt} - * authentication methods {@code (token_endpoint_auth_signing_alg)} - * @since 0.2.1 + * @return the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate the Client at the Token Endpoint + * @since 0.2.2 */ default String getTokenEndpointAuthenticationSigningAlgorithm() { return getClaimAsString(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG); @@ -140,6 +141,16 @@ public interface OidcClientMetadataClaimAccessor extends ClaimAccessor { return getClaimAsStringList(OidcClientMetadataClaimNames.SCOPE); } + /** + * Returns the {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)}. + * + * @return the {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)} + * @since 0.2.2 + */ + default URL getJwkSetUrl() { + return getClaimAsURL(OidcClientMetadataClaimNames.JWKS_URI); + } + /** * Returns the {@link SignatureAlgorithm JWS} algorithm required for signing the {@link OidcIdToken ID Token} issued to the Client {@code (id_token_signed_response_alg)}. * @@ -169,14 +180,4 @@ public interface OidcClientMetadataClaimAccessor extends ClaimAccessor { return getClaimAsURL(OidcClientMetadataClaimNames.REGISTRATION_CLIENT_URI); } - /** - * Returns {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)} - * - * @return {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)} - * @since 0.2.1 - */ - default URL getJwkSetUrl() { - return getClaimAsURL(OidcClientMetadataClaimNames.JWKS_URI); - } - } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimNames.java index eefe6706..475c50e9 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimNames.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimNames.java @@ -15,8 +15,9 @@ */ package org.springframework.security.oauth2.core.oidc; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; -import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.Jwt; /** * The names of the "claims" defined by OpenID Connect Dynamic Client Registration 1.0 @@ -64,6 +65,14 @@ public interface OidcClientMetadataClaimNames { */ String TOKEN_ENDPOINT_AUTH_METHOD = "token_endpoint_auth_method"; + /** + * {@code token_endpoint_auth_signing_alg} - the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} + * used to authenticate the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and + * {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods + * @since 0.2.2 + */ + String TOKEN_ENDPOINT_AUTH_SIGNING_ALG = "token_endpoint_auth_signing_alg"; + /** * {@code grant_types} - the OAuth 2.0 {@code grant_type} values that the Client will restrict itself to using */ @@ -79,6 +88,12 @@ public interface OidcClientMetadataClaimNames { */ String SCOPE = "scope"; + /** + * {@code jwks_uri} - the {@code URL} for the Client's JSON Web Key Set + * @since 0.2.2 + */ + String JWKS_URI = "jwks_uri"; + /** * {@code id_token_signed_response_alg} - the {@link JwsAlgorithm JWS} algorithm required for signing the {@link OidcIdToken ID Token} issued to the Client */ @@ -96,17 +111,4 @@ public interface OidcClientMetadataClaimNames { */ String REGISTRATION_CLIENT_URI = "registration_client_uri"; - /** - * {@code jwks_uri} - {@code URL} for the Client's JSON Web Key Set - * @since 0.2.1 - */ - String JWKS_URI = "jwks_uri"; - - /** - * {@code token_endpoint_auth_signing_alg} - {@link SignatureAlgorithm JWS} algorithm that must be used for signing - * the JWT used to authenticate the Client at the Token Endpoint for the {@code private_key_jwt} and {@code client_secret_jwt} - * authentication methods - * @since 0.2.1 - */ - String TOKEN_ENDPOINT_AUTH_SIGNING_ALG = "token_endpoint_auth_signing_alg"; } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistration.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistration.java index 3ccae919..45504030 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistration.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistration.java @@ -16,6 +16,8 @@ package org.springframework.security.oauth2.core.oidc; import java.io.Serializable; +import java.net.URI; +import java.net.URL; import java.time.Instant; import java.util.Collections; import java.util.LinkedHashMap; @@ -24,8 +26,11 @@ import java.util.List; import java.util.Map; import java.util.function.Consumer; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.Version; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.util.Assert; /** @@ -173,17 +178,17 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce } /** - * Sets the {@link SignatureAlgorithm JWS} algorithm that must be used for signing the JWT used to authenticate - * the Client at the Token Endpoint for the {@code private_key_jwt} and {@code client_secret_jwt} authentication - * methods - * @param signingAlgorithm the {@link SignatureAlgorithm JWS} algorithm that must be used for signing - * the JWT used to authenticate the Client at the Token Endpoint for the {@code private_key_jwt} and - * {@code client_secret_jwt} authentication methods + * Sets the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate + * the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and + * {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods, OPTIONAL. + + * @param authenticationSigningAlgorithm the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} + * used to authenticate the Client at the Token Endpoint * @return the {@link Builder} for further configuration - * @since 0.2.1 + * @since 0.2.2 */ - public Builder tokenEndpointAuthenticationSigningAlgorithm(String signingAlgorithm) { - return claim(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, signingAlgorithm); + public Builder tokenEndpointAuthenticationSigningAlgorithm(String authenticationSigningAlgorithm) { + return claim(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, authenticationSigningAlgorithm); } /** @@ -255,6 +260,17 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce return this; } + /** + * Sets the {@code URL} for the Client's JSON Web Key Set, OPTIONAL. + * + * @param jwkSetUrl the {@code URL} for the Client's JSON Web Key Set + * @return the {@link Builder} for further configuration + * @since 0.2.2 + */ + public Builder jwkSetUrl(String jwkSetUrl) { + return claim(OidcClientMetadataClaimNames.JWKS_URI, jwkSetUrl); + } + /** * Sets the {@link SignatureAlgorithm JWS} algorithm required for signing the {@link OidcIdToken ID Token} issued to the Client, OPTIONAL. * @@ -287,16 +303,6 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce return claim(OidcClientMetadataClaimNames.REGISTRATION_CLIENT_URI, registrationClientUrl); } - /** - * Sets {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)} - * @param jwksSetUrl {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)} - * @return the {@link Builder} for further configuration - * @since 0.2.1 - */ - public Builder jwkSetUrl(String jwksSetUrl) { - return claim(OidcClientMetadataClaimNames.JWKS_URI, jwksSetUrl); - } - /** * Sets the claim. * @@ -363,6 +369,9 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.SCOPE), "scope must be of type List"); Assert.notEmpty((List) this.claims.get(OidcClientMetadataClaimNames.SCOPE), "scope cannot be empty"); } + if (this.claims.get(OidcClientMetadataClaimNames.JWKS_URI) != null) { + validateURL(this.claims.get(OidcClientMetadataClaimNames.JWKS_URI), "jwksUri must be a valid URL"); + } } @SuppressWarnings("unchecked") @@ -382,5 +391,18 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce valuesConsumer.accept(values); } + private static void validateURL(Object url, String errorMessage) { + if (URL.class.isAssignableFrom(url.getClass())) { + return; + } + + try { + new URI(url.toString()).toURL(); + } catch (Exception ex) { + throw new IllegalArgumentException(errorMessage, ex); + } + } + } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcClientRegistrationHttpMessageConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcClientRegistrationHttpMessageConverter.java index 2fb73bf8..a87b76c1 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcClientRegistrationHttpMessageConverter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcClientRegistrationHttpMessageConverter.java @@ -15,6 +15,7 @@ */ package org.springframework.security.oauth2.core.oidc.http.converter; +import java.net.URL; import java.time.Instant; import java.util.Arrays; import java.util.Collection; @@ -130,6 +131,7 @@ public class OidcClientRegistrationHttpMessageConverter extends AbstractHttpMess private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class); private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class); private static final TypeDescriptor INSTANT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Instant.class); + private static final TypeDescriptor URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class); private static final Converter INSTANT_CONVERTER = getConverter(INSTANT_TYPE_DESCRIPTOR); private final ClaimTypeConverter claimTypeConverter; @@ -137,6 +139,7 @@ public class OidcClientRegistrationHttpMessageConverter extends AbstractHttpMess Converter stringConverter = getConverter(STRING_TYPE_DESCRIPTOR); Converter collectionStringConverter = getConverter( TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR)); + Converter urlConverter = getConverter(URL_TYPE_DESCRIPTOR); Map> claimConverters = new HashMap<>(); claimConverters.put(OidcClientMetadataClaimNames.CLIENT_ID, stringConverter); @@ -146,12 +149,12 @@ public class OidcClientRegistrationHttpMessageConverter extends AbstractHttpMess claimConverters.put(OidcClientMetadataClaimNames.CLIENT_NAME, stringConverter); claimConverters.put(OidcClientMetadataClaimNames.REDIRECT_URIS, collectionStringConverter); claimConverters.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, stringConverter); + claimConverters.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, stringConverter); claimConverters.put(OidcClientMetadataClaimNames.GRANT_TYPES, collectionStringConverter); claimConverters.put(OidcClientMetadataClaimNames.RESPONSE_TYPES, collectionStringConverter); claimConverters.put(OidcClientMetadataClaimNames.SCOPE, MapOidcClientRegistrationConverter::convertScope); + claimConverters.put(OidcClientMetadataClaimNames.JWKS_URI, urlConverter); claimConverters.put(OidcClientMetadataClaimNames.ID_TOKEN_SIGNED_RESPONSE_ALG, stringConverter); - claimConverters.put(OidcClientMetadataClaimNames.JWKS_URI, stringConverter); - claimConverters.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, stringConverter); this.claimTypeConverter = new ClaimTypeConverter(claimConverters); } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java index b1823e76..b92fe008 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java @@ -18,15 +18,17 @@ package org.springframework.security.oauth2.server.authorization.authentication; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; +import java.util.function.Predicate; + +import javax.crypto.spec.SecretKeySpec; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationProvider; @@ -49,6 +51,7 @@ import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; import org.springframework.security.oauth2.jose.jws.MacAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; import org.springframework.security.oauth2.jwt.JwtClaimValidator; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtDecoderFactory; @@ -61,9 +64,9 @@ import org.springframework.security.oauth2.server.authorization.client.Registere import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; - -import javax.crypto.spec.SecretKeySpec; +import org.springframework.web.util.UriComponentsBuilder; /** * An {@link AuthenticationProvider} implementation used for authenticating an OAuth 2.0 Client. @@ -80,16 +83,13 @@ import javax.crypto.spec.SecretKeySpec; * @see PasswordEncoder */ public final class OAuth2ClientAuthenticationProvider implements AuthenticationProvider { - private static final String CLIENT_AUTHENTICATION_ERROR_URI = "https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-3.2.1"; - + private static final String CLIENT_AUTHENTICATION_ERROR_URI = "https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-3.2.1"; private static final ClientAuthenticationMethod JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD = new ClientAuthenticationMethod("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); - private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE); private final RegisteredClientRepository registeredClientRepository; private final OAuth2AuthorizationService authorizationService; - private JwtDecoderFactory jwtDecoderFactory; - private ProviderSettings providerSettings; + private final JwtClientAssertionDecoderFactory jwtClientAssertionDecoderFactory; private PasswordEncoder passwordEncoder; /** @@ -104,8 +104,8 @@ public final class OAuth2ClientAuthenticationProvider implements AuthenticationP Assert.notNull(authorizationService, "authorizationService cannot be null"); this.registeredClientRepository = registeredClientRepository; this.authorizationService = authorizationService; + this.jwtClientAssertionDecoderFactory = new JwtClientAssertionDecoderFactory(); this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); - this.jwtDecoderFactory = new RegisteredClientJwtAssertionDecoderFactory(); } /** @@ -123,7 +123,7 @@ public final class OAuth2ClientAuthenticationProvider implements AuthenticationP @Autowired protected void setProviderSettings(ProviderSettings providerSettings) { - this.providerSettings = providerSettings; + this.jwtClientAssertionDecoderFactory.setProviderSettings(providerSettings); } @Override @@ -132,11 +132,16 @@ public final class OAuth2ClientAuthenticationProvider implements AuthenticationP (OAuth2ClientAuthenticationToken) authentication; return JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod()) ? - authenticateClientAssertion(authentication) : - authenticationClientCredentials(authentication); + authenticateJwtClientAssertion(authentication) : + authenticateClientCredentials(authentication); } - private Authentication authenticationClientCredentials(Authentication authentication) throws AuthenticationException { + @Override + public boolean supports(Class authentication) { + return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication); + } + + private Authentication authenticateClientCredentials(Authentication authentication) throws AuthenticationException { OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) authentication; @@ -171,7 +176,7 @@ public final class OAuth2ClientAuthenticationProvider implements AuthenticationP clientAuthentication.getClientAuthenticationMethod(), clientAuthentication.getCredentials()); } - private Authentication authenticateClientAssertion(Authentication authentication) throws AuthenticationException { + private Authentication authenticateJwtClientAssertion(Authentication authentication) throws AuthenticationException { OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) authentication; @@ -181,26 +186,20 @@ public final class OAuth2ClientAuthenticationProvider implements AuthenticationP throwInvalidClient(OAuth2ParameterNames.CLIENT_ID); } - Set allowedAuthenticationMethods = registeredClient.getClientAuthenticationMethods(); - - if (!allowedAuthenticationMethods.contains(ClientAuthenticationMethod.CLIENT_SECRET_JWT) && - !allowedAuthenticationMethods.contains(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { + if (!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.PRIVATE_KEY_JWT) && + !registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) { throwInvalidClient("authentication_method"); } boolean credentialsAuthenticated = false; + Jwt jwtAssertion = null; + JwtDecoder jwtDecoder = this.jwtClientAssertionDecoderFactory.createDecoder(registeredClient); try { - JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(registeredClient); - Jwt jwt = jwtDecoder.decode(clientAuthentication.getCredentials().toString()); - List aud = jwt.getClaimAsStringList("aud"); - String issuer = getIssuerUri(clientAuthentication.getRequestUri()); - if (aud == null || !aud.contains(issuer)) { - throwInvalidClient(OAuth2ParameterNames.CLIENT_ASSERTION); - } + jwtAssertion = jwtDecoder.decode(clientAuthentication.getCredentials().toString()); credentialsAuthenticated = true; - } catch (JwtException e) { - throwInvalidClient(OAuth2ParameterNames.CLIENT_ASSERTION); + } catch (JwtException ex) { + throwInvalidClient(OAuth2ParameterNames.CLIENT_ASSERTION, ex); } boolean pkceAuthenticated = authenticatePkceIfAvailable(clientAuthentication, registeredClient); @@ -209,29 +208,12 @@ public final class OAuth2ClientAuthenticationProvider implements AuthenticationP throwInvalidClient("credentials"); } - JwsAlgorithm tokenEndpointSigningAlgorithm = registeredClient.getClientSettings().getTokenEndpointSigningAlgorithm(); - ClientAuthenticationMethod clientAuthentiationMethod = tokenEndpointSigningAlgorithm instanceof MacAlgorithm ? - ClientAuthenticationMethod.CLIENT_SECRET_JWT : ClientAuthenticationMethod.PRIVATE_KEY_JWT; + ClientAuthenticationMethod clientAuthenticationMethod = + registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm() instanceof SignatureAlgorithm ? + ClientAuthenticationMethod.PRIVATE_KEY_JWT : + ClientAuthenticationMethod.CLIENT_SECRET_JWT; - return new OAuth2ClientAuthenticationToken(registeredClient, - clientAuthentiationMethod, clientAuthentication.getCredentials()); - } - - private String getIssuerUri(String requestUri) throws AuthenticationException { - if (requestUri.endsWith(providerSettings.getTokenEndpoint())) { - return providerSettings.getIssuer() + providerSettings.getTokenEndpoint(); - } else if (requestUri.endsWith(providerSettings.getTokenIntrospectionEndpoint())) { - return providerSettings.getIssuer() + providerSettings.getTokenIntrospectionEndpoint(); - } else if (requestUri.endsWith(providerSettings.getTokenRevocationEndpoint())) { - return providerSettings.getIssuer() + providerSettings.getTokenRevocationEndpoint(); - } - throwInvalidClient(OAuth2ParameterNames.CLIENT_ASSERTION); - return null; - } - - @Override - public boolean supports(Class authentication) { - return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication); + return new OAuth2ClientAuthenticationToken(registeredClient, clientAuthenticationMethod, jwtAssertion); } private boolean authenticatePkceIfAvailable(OAuth2ClientAuthenticationToken clientAuthentication, @@ -298,29 +280,21 @@ public final class OAuth2ClientAuthenticationProvider implements AuthenticationP } private static void throwInvalidClient(String parameterName) { + throwInvalidClient(parameterName, null); + } + + private static void throwInvalidClient(String parameterName, Throwable cause) { OAuth2Error error = new OAuth2Error( OAuth2ErrorCodes.INVALID_CLIENT, "Client authentication failed: " + parameterName, CLIENT_AUTHENTICATION_ERROR_URI); - throw new OAuth2AuthenticationException(error); + throw new OAuth2AuthenticationException(error, error.toString(), cause); } - private static class CachedJwtDecoder { - private final NimbusJwtDecoder jwtDecoder; - private final RegisteredClient registeredClient; - - CachedJwtDecoder(NimbusJwtDecoder jwtDecoder, RegisteredClient registeredClient) { - this.jwtDecoder = jwtDecoder; - this.registeredClient = registeredClient; - } - } - - private static class RegisteredClientJwtAssertionDecoderFactory implements JwtDecoderFactory { - - private static final String CLIENT_ASSERTION_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7523#section-3"; + private static class JwtClientAssertionDecoderFactory implements JwtDecoderFactory { + private static final String JWT_CLIENT_AUTHENTICATION_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7523#section-3"; private static final Map JCA_ALGORITHM_MAPPINGS; - static { Map mappings = new HashMap<>(); mappings.put(MacAlgorithm.HS256, "HmacSHA256"); @@ -329,68 +303,100 @@ public final class OAuth2ClientAuthenticationProvider implements AuthenticationP JCA_ALGORITHM_MAPPINGS = Collections.unmodifiableMap(mappings); } - private final Function jwsAlgorithmResolver = - rc -> rc.getClientSettings().getTokenEndpointSigningAlgorithm(); + private final Map jwtDecoders = new ConcurrentHashMap<>(); + private List providerAudience = Collections.emptyList(); - private final Map cachedDecoders = new ConcurrentHashMap<>(); + private void setProviderSettings(ProviderSettings providerSettings) { + this.providerAudience = getProviderAudience(providerSettings); + } @Override public JwtDecoder createDecoder(RegisteredClient registeredClient) { Assert.notNull(registeredClient, "registeredClient cannot be null"); - - CachedJwtDecoder cachedDecoder = this.cachedDecoders.get(registeredClient.getClientId()); - if (cachedDecoder != null && registeredClient.equals(cachedDecoder.registeredClient)) { - return cachedDecoder.jwtDecoder; - } - - cachedDecoder = new CachedJwtDecoder(buildDecoder(registeredClient), registeredClient); - cachedDecoder.jwtDecoder.setJwtValidator(createTokenValidator(registeredClient)); - this.cachedDecoders.put(registeredClient.getClientId(), cachedDecoder); - return cachedDecoder.jwtDecoder; + return this.jwtDecoders.computeIfAbsent(registeredClient.getId(), (key) -> { + NimbusJwtDecoder jwtDecoder = buildDecoder(registeredClient); + jwtDecoder.setJwtValidator(createJwtValidator(registeredClient)); + return jwtDecoder; + }); } private NimbusJwtDecoder buildDecoder(RegisteredClient registeredClient) { - JwsAlgorithm jwsAlgorithm = this.jwsAlgorithmResolver.apply(registeredClient); - - if (jwsAlgorithm != null && SignatureAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) { + JwsAlgorithm jwsAlgorithm = registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm(); + if (jwsAlgorithm instanceof SignatureAlgorithm) { String jwkSetUrl = registeredClient.getClientSettings().getJwkSetUrl(); if (!StringUtils.hasText(jwkSetUrl)) { OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT, - "misconfigured client", CLIENT_ASSERTION_ERROR_URI); - throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + "Failed to find a Signature Verifier for Client: '" + + registeredClient.getId() + + "'. Check to ensure you have configured the JWK Set URL.", + JWT_CLIENT_AUTHENTICATION_ERROR_URI); + throw new OAuth2AuthenticationException(oauth2Error); } return NimbusJwtDecoder.withJwkSetUri(jwkSetUrl).jwsAlgorithm((SignatureAlgorithm) jwsAlgorithm).build(); } - - if (jwsAlgorithm != null && MacAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) { + if (jwsAlgorithm instanceof MacAlgorithm) { String clientSecret = registeredClient.getClientSecret(); if (!StringUtils.hasText(clientSecret)) { OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT, - "misconfigured client", CLIENT_ASSERTION_ERROR_URI); - throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + "Failed to find a Signature Verifier for Client: '" + + registeredClient.getId() + + "'. Check to ensure you have configured the client secret.", + JWT_CLIENT_AUTHENTICATION_ERROR_URI); + throw new OAuth2AuthenticationException(oauth2Error); } SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8), JCA_ALGORITHM_MAPPINGS.get(jwsAlgorithm)); return NimbusJwtDecoder.withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build(); } - OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT, - "misconfigured client", CLIENT_ASSERTION_ERROR_URI); - throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + "Failed to find a Signature Verifier for Client: '" + + registeredClient.getId() + + "'. Check to ensure you have configured a valid JWS Algorithm: '" + jwsAlgorithm + "'.", + JWT_CLIENT_AUTHENTICATION_ERROR_URI); + throw new OAuth2AuthenticationException(oauth2Error); } - private OAuth2TokenValidator createTokenValidator(RegisteredClient registeredClient) { + private OAuth2TokenValidator createJwtValidator(RegisteredClient registeredClient) { String clientId = registeredClient.getClientId(); return new DelegatingOAuth2TokenValidator<>( - new JwtClaimValidator("iss", clientId::equals), // RFC 7523 section 3 (iss) - new JwtClaimValidator("sub", clientId::equals), // RFC 7523 section 3 (sub) - new JwtClaimValidator<>("exp", Objects::nonNull), // RFC 7523 section 3 (exp != null) - new JwtTimestampValidator() // RFC 7523 section 3 (exp, nbf) + new JwtClaimValidator<>(JwtClaimNames.ISS, clientId::equals), + new JwtClaimValidator<>(JwtClaimNames.SUB, clientId::equals), + new JwtClaimValidator<>(JwtClaimNames.AUD, containsProviderAudience()), + new JwtClaimValidator<>(JwtClaimNames.EXP, Objects::nonNull), + new JwtTimestampValidator() ); - // The `aud` claim is not verified here - - // TODO RFC 7523 section 3 #7: JWT may contain "jti" claim that provides unique identified for the token (OPTIONAL) } + + private Predicate> containsProviderAudience() { + return (audienceClaim) -> { + if (CollectionUtils.isEmpty(audienceClaim)) { + return false; + } + for (String audience : audienceClaim) { + if (this.providerAudience.contains(audience)) { + return true; + } + } + return false; + }; + } + + private static List getProviderAudience(ProviderSettings providerSettings) { + if (!StringUtils.hasText(providerSettings.getIssuer())) { + return Collections.emptyList(); + } + List providerAudience = new ArrayList<>(); + providerAudience.add(providerSettings.getIssuer()); + providerAudience.add(asUrl(providerSettings.getIssuer(), providerSettings.getTokenEndpoint())); + providerAudience.add(asUrl(providerSettings.getIssuer(), providerSettings.getTokenIntrospectionEndpoint())); + providerAudience.add(asUrl(providerSettings.getIssuer(), providerSettings.getTokenRevocationEndpoint())); + return providerAudience; + } + + private static String asUrl(String issuer, String endpoint) { + return UriComponentsBuilder.fromUriString(issuer).path(endpoint).build().toUriString(); + } + } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationToken.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationToken.java index c46a0b5d..53e64e86 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationToken.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationToken.java @@ -41,7 +41,6 @@ import org.springframework.util.Assert; @Transient public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = Version.SERIAL_VERSION_UID; - private final String requestUri; private final String clientId; private final RegisteredClient registeredClient; private final ClientAuthenticationMethod clientAuthenticationMethod; @@ -56,37 +55,11 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken * @param credentials the client credentials * @param additionalParameters the additional parameters */ - @Deprecated public OAuth2ClientAuthenticationToken(String clientId, ClientAuthenticationMethod clientAuthenticationMethod, @Nullable Object credentials, @Nullable Map additionalParameters) { super(Collections.emptyList()); Assert.hasText(clientId, "clientId cannot be empty"); Assert.notNull(clientAuthenticationMethod, "clientAuthenticationMethod cannot be null"); - this.requestUri = null; - this.clientId = clientId; - this.registeredClient = null; - this.clientAuthenticationMethod = clientAuthenticationMethod; - this.credentials = credentials; - this.additionalParameters = Collections.unmodifiableMap( - additionalParameters != null ? additionalParameters : Collections.emptyMap()); - } - - /** - * Constructs an {@code OAuth2ClientAuthenticationToken} using the provided parameters. - * - * @param requestUri the issuer identifier - * @param clientId the client identifier - * @param clientAuthenticationMethod the authentication method used by the client - * @param credentials the client credentials - * @param additionalParameters the additional parameters - */ - public OAuth2ClientAuthenticationToken(String requestUri, String clientId, ClientAuthenticationMethod clientAuthenticationMethod, - @Nullable Object credentials, @Nullable Map additionalParameters) { - super(Collections.emptyList()); - Assert.hasText(requestUri, "requestUri cannot be empty"); - Assert.hasText(clientId, "clientId cannot be empty"); - Assert.notNull(clientAuthenticationMethod, "clientAuthenticationMethod cannot be null"); - this.requestUri = requestUri; this.clientId = clientId; this.registeredClient = null; this.clientAuthenticationMethod = clientAuthenticationMethod; @@ -107,7 +80,6 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken super(Collections.emptyList()); Assert.notNull(registeredClient, "registeredClient cannot be null"); Assert.notNull(clientAuthenticationMethod, "clientAuthenticationMethod cannot be null"); - this.requestUri = null; this.clientId = registeredClient.getClientId(); this.registeredClient = registeredClient; this.clientAuthenticationMethod = clientAuthenticationMethod; @@ -146,15 +118,6 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken return this.clientAuthenticationMethod; } - /** - * Returns URI of authenticated request. This is used to validate aud claim of JWT client assertions - * - * @return URI of authenticated request - */ - public String getRequestUri() { - return requestUri; - } - /** * Returns the additional parameters. * diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ClientSettings.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ClientSettings.java index 8cdf0c27..c37e942e 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ClientSettings.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ClientSettings.java @@ -17,8 +17,9 @@ package org.springframework.security.oauth2.server.authorization.config; import java.util.Map; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; -import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.util.Assert; /** @@ -56,24 +57,25 @@ public final class ClientSettings extends AbstractSettings { } /** - * Returns {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)} - * @return {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)} - * @since 0.2.1 + * Returns the {@code URL} for the Client's JSON Web Key Set. + * + * @return the {@code URL} for the Client's JSON Web Key Set + * @since 0.2.2 */ public String getJwkSetUrl() { return getSetting(ConfigurationSettingNames.Client.JWK_SET_URL); } /** - * Returns {@link SignatureAlgorithm JWS} algorithm that must be used for signing the JWT used to authenticate the - * Client at the Token Endpoint for the {@code private_key_jwt} and {@code client_secret_jwt} authentication methods - * @return {@link SignatureAlgorithm JWS} algorithm that must be used for signing the JWT used to authenticate the - * Client at the Token Endpoint for the {@code private_key_jwt} and {@code client_secret_jwt} authentication - * methods - * @since 0.2.1 + * Returns the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate + * the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and + * {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods. + * + * @return the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate the Client at the Token Endpoint + * @since 0.2.2 */ - public JwsAlgorithm getTokenEndpointSigningAlgorithm() { - return getSetting(ConfigurationSettingNames.Client.TOKEN_ENDPOINT_SIGNING_ALGORITHM); + public JwsAlgorithm getTokenEndpointAuthenticationSigningAlgorithm() { + return getSetting(ConfigurationSettingNames.Client.TOKEN_ENDPOINT_AUTHENTICATION_SIGNING_ALGORITHM); } /** @@ -84,8 +86,7 @@ public final class ClientSettings extends AbstractSettings { public static Builder builder() { return new Builder() .requireProofKey(false) - .requireAuthorizationConsent(false) - .tokenEndpointSigningAlgorithm(SignatureAlgorithm.RS256); + .requireAuthorizationConsent(false); } /** @@ -131,28 +132,28 @@ public final class ClientSettings extends AbstractSettings { } /** - * Sets {@code URL} for the Client's JSON Web Key Set + * Sets the {@code URL} for the Client's JSON Web Key Set. * - * @param jwkSetUrl {@code URL} for the Client's JSON Web Key Set + * @param jwkSetUrl the {@code URL} for the Client's JSON Web Key Set * @return the {@link Builder} for further configuration - * @since 0.2.1 + * @since 0.2.2 */ public Builder jwkSetUrl(String jwkSetUrl) { return setting(ConfigurationSettingNames.Client.JWK_SET_URL, jwkSetUrl); } /** - * Sets {@link SignatureAlgorithm JWS} algorithm that must be used for signing the JWT used to authenticate the - * Client at the Token Endpoint for the {@code private_key_jwt} and {@code client_secret_jwt} authentication methods - * - * @param signingAlgorithm {@link SignatureAlgorithm JWS} algorithm that must be used for signing - * the JWT used to authenticate the Client at the Token Endpoint for the {@code private_key_jwt} and - * {@code client_secret_jwt} authentication methods + * Sets the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate + * the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and + * {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods. + + * @param authenticationSigningAlgorithm the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} + * used to authenticate the Client at the Token Endpoint * @return the {@link Builder} for further configuration - * @since 0.2.1 + * @since 0.2.2 */ - public Builder tokenEndpointSigningAlgorithm(JwsAlgorithm signingAlgorithm) { - return setting(ConfigurationSettingNames.Client.TOKEN_ENDPOINT_SIGNING_ALGORITHM, signingAlgorithm); + public Builder tokenEndpointAuthenticationSigningAlgorithm(JwsAlgorithm authenticationSigningAlgorithm) { + return setting(ConfigurationSettingNames.Client.TOKEN_ENDPOINT_AUTHENTICATION_SIGNING_ALGORITHM, authenticationSigningAlgorithm); } /** diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ConfigurationSettingNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ConfigurationSettingNames.java index 1a8523a6..d9c0f88c 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ConfigurationSettingNames.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ConfigurationSettingNames.java @@ -15,8 +15,11 @@ */ package org.springframework.security.oauth2.server.authorization.config; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.Jwt; /** * The names for all the configuration settings. @@ -49,15 +52,18 @@ public final class ConfigurationSettingNames { public static final String REQUIRE_AUTHORIZATION_CONSENT = CLIENT_SETTINGS_NAMESPACE.concat("require-authorization-consent"); /** - * {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)} + * Set the {@code URL} for the Client's JSON Web Key Set. + * @since 0.2.2 */ public static final String JWK_SET_URL = CLIENT_SETTINGS_NAMESPACE.concat("jwk-set-url"); /** - * {@link SignatureAlgorithm JWS} algorithm that must be used for signing the JWT used to authenticate the - * Client at the Token Endpoint for the {@code private_key_jwt} and {@code client_secret_jwt} authentication methods + * Set the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} + * used to authenticate the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and + * {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods. + * @since 0.2.2 */ - public static final String TOKEN_ENDPOINT_SIGNING_ALGORITHM = CLIENT_SETTINGS_NAMESPACE.concat("token-endpoint-signing-algorithm"); + public static final String TOKEN_ENDPOINT_AUTHENTICATION_SIGNING_ALGORITHM = CLIENT_SETTINGS_NAMESPACE.concat("token-endpoint-authentication-signing-algorithm"); private Client() { } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/SignatureAlgorithmMixin.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/JwsAlgorithmMixin.java similarity index 96% rename from oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/SignatureAlgorithmMixin.java rename to oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/JwsAlgorithmMixin.java index d2a9e1d5..eee09cc4 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/SignatureAlgorithmMixin.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/JwsAlgorithmMixin.java @@ -30,5 +30,5 @@ import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE) -abstract class SignatureAlgorithmMixin { +abstract class JwsAlgorithmMixin { } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/MacAlgorithmMixin.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/MacAlgorithmMixin.java deleted file mode 100644 index bbb934e4..00000000 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/MacAlgorithmMixin.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.oauth2.server.authorization.jackson2; - -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import org.springframework.security.oauth2.jose.jws.MacAlgorithm; - -/** - * This mixin class is used to serialize/deserialize {@link MacAlgorithm}. - * - * @author Rafal Lewczuk - * @since 0.2.1 - * @see MacAlgorithm - */ -@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) -@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, - isGetterVisibility = JsonAutoDetect.Visibility.NONE) -public class MacAlgorithmMixin { -} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java index b067ec87..c1497d14 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java @@ -37,7 +37,7 @@ import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; *
  • {@link HashSetMixin}
  • *
  • {@link OAuth2AuthorizationRequestMixin}
  • *
  • {@link DurationMixin}
  • - *
  • {@link SignatureAlgorithmMixin}
  • + *
  • {@link JwsAlgorithmMixin}
  • * * * If not already enabled, default typing will be automatically enabled as type info is @@ -59,7 +59,7 @@ import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; * @see HashSetMixin * @see OAuth2AuthorizationRequestMixin * @see DurationMixin - * @see SignatureAlgorithmMixin + * @see JwsAlgorithmMixin */ public class OAuth2AuthorizationServerJackson2Module extends SimpleModule { @@ -76,8 +76,8 @@ public class OAuth2AuthorizationServerJackson2Module extends SimpleModule { context.setMixInAnnotations(LinkedHashSet.class, HashSetMixin.class); context.setMixInAnnotations(OAuth2AuthorizationRequest.class, OAuth2AuthorizationRequestMixin.class); context.setMixInAnnotations(Duration.class, DurationMixin.class); - context.setMixInAnnotations(SignatureAlgorithm.class, SignatureAlgorithmMixin.class); - context.setMixInAnnotations(MacAlgorithm.class, MacAlgorithmMixin.class); + context.setMixInAnnotations(SignatureAlgorithm.class, JwsAlgorithmMixin.class); + context.setMixInAnnotations(MacAlgorithm.class, JwsAlgorithmMixin.class); } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java index e525fbbd..234931f2 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java @@ -36,12 +36,13 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2TokenType; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.oidc.OidcClientMetadataClaimNames; import org.springframework.security.oauth2.core.oidc.OidcClientRegistration; -import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; import org.springframework.security.oauth2.jose.jws.MacAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.JoseHeader; @@ -58,6 +59,7 @@ import org.springframework.security.oauth2.server.authorization.config.TokenSett import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponentsBuilder; /** @@ -196,13 +198,12 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe if (!isValidRedirectUris(clientRegistrationAuthentication.getClientRegistration().getRedirectUris())) { // TODO Add OAuth2ErrorCodes.INVALID_REDIRECT_URI - throw new OAuth2AuthenticationException("invalid_redirect_uri"); + throwInvalidClientRegistration("invalid_redirect_uri", OidcClientMetadataClaimNames.REDIRECT_URIS); } - if (!isValidJwtClientAuthenticationMetadata(clientRegistrationAuthentication.getClientRegistration())) { + if (!isValidTokenEndpointAuthenticationMethod(clientRegistrationAuthentication.getClientRegistration())) { // TODO Add OAuth2ErrorCodes.INVALID_CLIENT_METADATA - // TODO populate "error_description" - throw new OAuth2AuthenticationException("invalid_client_metadata"); + throwInvalidClientRegistration("invalid_client_metadata", OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD); } RegisteredClient registeredClient = createClient(clientRegistrationAuthentication.getClientRegistration()); @@ -298,8 +299,8 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe builder.jwkSetUrl(clientSettings.getJwkSetUrl()); } - if (clientSettings.getTokenEndpointSigningAlgorithm() != null) { - builder.tokenEndpointAuthenticationSigningAlgorithm(clientSettings.getTokenEndpointSigningAlgorithm().getName()); + if (clientSettings.getTokenEndpointAuthenticationSigningAlgorithm() != null) { + builder.tokenEndpointAuthenticationSigningAlgorithm(clientSettings.getTokenEndpointAuthenticationSigningAlgorithm().getName()); } return builder; @@ -347,29 +348,28 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe return true; } - private static boolean isValidJwtClientAuthenticationMetadata(OidcClientRegistration clientRegistration) { + private static boolean isValidTokenEndpointAuthenticationMethod(OidcClientRegistration clientRegistration) { String authenticationMethod = clientRegistration.getTokenEndpointAuthenticationMethod(); - String signingAlgorithm = clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm(); + String authenticationSigningAlgorithm = clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm(); - if ("none".equals(signingAlgorithm)) { + if (!ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equals(authenticationMethod) && + !ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue().equals(authenticationMethod)) { + return !StringUtils.hasText(authenticationSigningAlgorithm); + } + + if ("none".equals(authenticationSigningAlgorithm)) { return false; } - if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue().equals(authenticationMethod)) { - return signingAlgorithm == null || MacAlgorithm.from(signingAlgorithm) != null; - } - if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equals(authenticationMethod)) { - try { - return clientRegistration.getJwkSetUrl() != null && (signingAlgorithm == null - || SignatureAlgorithm.from(signingAlgorithm) != null); - } catch (IllegalArgumentException e) { - return false; - } + return clientRegistration.getJwkSetUrl() != null && + (!StringUtils.hasText(authenticationSigningAlgorithm) || + SignatureAlgorithm.from(authenticationSigningAlgorithm) != null); + } else { + // client_secret_jwt + return !StringUtils.hasText(authenticationSigningAlgorithm) || + MacAlgorithm.from(authenticationSigningAlgorithm) != null; } - - // TODO return false if token_endpoint_auth_signing_alg or jwks_uri exists but authentication method is not client_secret_jwt nor private_key_jwt ? - return true; } private static RegisteredClient createClient(OidcClientRegistration clientRegistration) { @@ -387,7 +387,7 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe } else if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) { builder.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT); } else { - builder.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC); + builder.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC); } builder.redirectUris(redirectUris -> @@ -414,16 +414,18 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe .requireProofKey(true) .requireAuthorizationConsent(true); - String signatureAlgorithm = clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm(); - if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) { - JwsAlgorithm macAlgorithm = signatureAlgorithm != null ? MacAlgorithm.from(signatureAlgorithm) : MacAlgorithm.HS256; - clientSettingsBuilder.tokenEndpointSigningAlgorithm(macAlgorithm); - } - - if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) { - JwsAlgorithm jwsAlgorithm = signatureAlgorithm != null ? SignatureAlgorithm.from(signatureAlgorithm) : SignatureAlgorithm.RS256; - clientSettingsBuilder.tokenEndpointSigningAlgorithm(jwsAlgorithm); + MacAlgorithm macAlgorithm = MacAlgorithm.from(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm()); + if (macAlgorithm == null) { + macAlgorithm = MacAlgorithm.HS256; + } + clientSettingsBuilder.tokenEndpointAuthenticationSigningAlgorithm(macAlgorithm); + } else if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) { + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.from(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm()); + if (signatureAlgorithm == null) { + signatureAlgorithm = SignatureAlgorithm.RS256; + } + clientSettingsBuilder.tokenEndpointAuthenticationSigningAlgorithm(signatureAlgorithm); clientSettingsBuilder.jwkSetUrl(clientRegistration.getJwkSetUrl().toString()); } @@ -437,4 +439,12 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe // @formatter:on } + private static void throwInvalidClientRegistration(String errorCode, String fieldName) { + OAuth2Error error = new OAuth2Error( + errorCode, + "Invalid Client Registration: " + fieldName, + "https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationError"); + throw new OAuth2AuthenticationException(error); + } + } 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 b5d98909..9ae71db2 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 @@ -15,6 +15,15 @@ */ package org.springframework.security.oauth2.server.authorization.oidc.web; +import java.io.IOException; +import java.util.List; +import java.util.function.Consumer; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.server.ServletServerHttpResponse; @@ -32,12 +41,6 @@ import org.springframework.util.Assert; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.UriComponentsBuilder; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - /** * A {@code Filter} that processes OpenID Provider Configuration Requests. * @@ -80,11 +83,8 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques .issuer(this.providerSettings.getIssuer()) .authorizationEndpoint(asUrl(this.providerSettings.getIssuer(), this.providerSettings.getAuthorizationEndpoint())) .tokenEndpoint(asUrl(this.providerSettings.getIssuer(), this.providerSettings.getTokenEndpoint())) - .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()) - .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()) + .tokenEndpointAuthenticationMethods(clientAuthenticationMethods()) .jwkSetUrl(asUrl(this.providerSettings.getIssuer(), this.providerSettings.getJwkSetEndpoint())) - .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()) - .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()) .responseType(OAuth2AuthorizationResponseType.CODE.getValue()) .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) .grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) @@ -99,6 +99,15 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques providerConfiguration, MediaType.APPLICATION_JSON, httpResponse); } + private static Consumer> clientAuthenticationMethods() { + return (authenticationMethods) -> { + authenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()); + authenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()); + authenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()); + authenticationMethods.add(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()); + }; + } + private static String asUrl(String issuer, String endpoint) { return UriComponentsBuilder.fromUriString(issuer).path(endpoint).build().toUriString(); } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java index b23d9727..3975badb 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java @@ -42,8 +42,8 @@ import org.springframework.security.oauth2.server.authorization.authentication.O import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretBasicAuthenticationConverter; import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretPostAuthenticationConverter; import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter; -import org.springframework.security.oauth2.server.authorization.web.authentication.PublicClientAuthenticationConverter; import org.springframework.security.oauth2.server.authorization.web.authentication.JwtClientAssertionAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.PublicClientAuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; @@ -87,9 +87,9 @@ public final class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter this.requestMatcher = requestMatcher; this.authenticationConverter = new DelegatingAuthenticationConverter( Arrays.asList( + new JwtClientAssertionAuthenticationConverter(), new ClientSecretBasicAuthenticationConverter(), new ClientSecretPostAuthenticationConverter(), - new JwtClientAssertionAuthenticationConverter(), new PublicClientAuthenticationConverter())); } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/ClientSecretBasicAuthenticationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/ClientSecretBasicAuthenticationConverter.java index a244d567..13d9bd42 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/ClientSecretBasicAuthenticationConverter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/ClientSecretBasicAuthenticationConverter.java @@ -89,6 +89,7 @@ public final class ClientSecretBasicAuthenticationConverter implements Authentic } return new OAuth2ClientAuthenticationToken(clientID, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, clientSecret, - OAuth2EndpointUtils.extractAdditionalParameters(request)); + OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(request)); } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/ClientSecretPostAuthenticationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/ClientSecretPostAuthenticationConverter.java index 98720173..04589c97 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/ClientSecretPostAuthenticationConverter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/ClientSecretPostAuthenticationConverter.java @@ -69,10 +69,12 @@ public final class ClientSecretPostAuthenticationConverter implements Authentica throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); } - Map additionalParameters = OAuth2EndpointUtils.extractAdditionalParameters(request, - OAuth2ParameterNames.CLIENT_ID, OAuth2ParameterNames.CLIENT_SECRET); + Map additionalParameters = OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(request, + OAuth2ParameterNames.CLIENT_ID, + OAuth2ParameterNames.CLIENT_SECRET); return new OAuth2ClientAuthenticationToken(clientId, ClientAuthenticationMethod.CLIENT_SECRET_POST, clientSecret, additionalParameters); } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/JwtClientAssertionAuthenticationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/JwtClientAssertionAuthenticationConverter.java index b145ea78..59165ff2 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/JwtClientAssertionAuthenticationConverter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/JwtClientAssertionAuthenticationConverter.java @@ -13,9 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.security.oauth2.server.authorization.web.authentication; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + import org.springframework.lang.Nullable; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; @@ -28,65 +31,59 @@ import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; -import javax.servlet.http.HttpServletRequest; -import java.util.Map; - /** - * Attempts to extract client assertion credentials from {@link HttpServletRequest} + * Attempts to extract a JWT client assertion credential from {@link HttpServletRequest} * and then converts to an {@link OAuth2ClientAuthenticationToken} used for authenticating the client. * * @author Rafal Lewczuk - * @since 0.2.1 + * @since 0.2.2 * @see AuthenticationConverter * @see OAuth2ClientAuthenticationToken * @see OAuth2ClientAuthenticationFilter */ public final class JwtClientAssertionAuthenticationConverter implements AuthenticationConverter { - - private static final ClientAuthenticationMethod JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD - = new ClientAuthenticationMethod("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); + private static final ClientAuthenticationMethod JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD = + new ClientAuthenticationMethod("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); @Nullable @Override public Authentication convert(HttpServletRequest request) { - MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); - - // client_assertion_type (REQUIRED), client_assertion (REQUIRED) - String clientJwtAssertionType = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE); - String clientJwtAssertion = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION); - - if (!StringUtils.hasText(clientJwtAssertionType) || !StringUtils.hasText(clientJwtAssertion)) { + if (request.getParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE) == null || + request.getParameter(OAuth2ParameterNames.CLIENT_ASSERTION) == null) { return null; } + MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); + + // client_assertion_type (REQUIRED) + String clientAssertionType = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE); if (parameters.get(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE).size() != 1) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); } - - if (parameters.get(OAuth2ParameterNames.CLIENT_ASSERTION).size() != 1) { - throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); + if (!JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.getValue().equals(clientAssertionType)) { + return null; } - if (!JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.getValue().equals(clientJwtAssertionType)) { + // client_assertion (REQUIRED) + String jwtAssertion = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION); + if (parameters.get(OAuth2ParameterNames.CLIENT_ASSERTION).size() != 1) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); } // client_id (OPTIONAL as per specification but REQUIRED by this implementation) String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID); - if (!StringUtils.hasText(clientId)) { + if (!StringUtils.hasText(clientId) || + parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); } - if (parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) { - throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); - } - - Map additionalParameters = OAuth2EndpointUtils.extractAdditionalParameters(request, - OAuth2ParameterNames.CLIENT_ID, + Map additionalParameters = OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(request, OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, - OAuth2ParameterNames.CLIENT_ASSERTION); + OAuth2ParameterNames.CLIENT_ASSERTION, + OAuth2ParameterNames.CLIENT_ID); - return new OAuth2ClientAuthenticationToken(request.getRequestURI(), clientId, - JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, clientJwtAssertion, additionalParameters); + return new OAuth2ClientAuthenticationToken(clientId, JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, + jwtAssertion, additionalParameters); } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2EndpointUtils.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2EndpointUtils.java index 3a8a6ad5..78f2e411 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2EndpointUtils.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2EndpointUtils.java @@ -54,6 +54,17 @@ final class OAuth2EndpointUtils { return parameters; } + static Map getParametersIfMatchesAuthorizationCodeGrantRequest(HttpServletRequest request, String... exclusions) { + if (!matchesAuthorizationCodeGrantRequest(request)) { + return Collections.emptyMap(); + } + Map parameters = new HashMap<>(getParameters(request).toSingleValueMap()); + for (String exclusion : exclusions) { + parameters.remove(exclusion); + } + return parameters; + } + static boolean matchesAuthorizationCodeGrantRequest(HttpServletRequest request) { return AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals( request.getParameter(OAuth2ParameterNames.GRANT_TYPE)) && @@ -70,14 +81,4 @@ final class OAuth2EndpointUtils { throw new OAuth2AuthenticationException(error); } - static Map extractAdditionalParameters(HttpServletRequest request, String...exclusions) { - Map additionalParameters = Collections.emptyMap(); - if (OAuth2EndpointUtils.matchesAuthorizationCodeGrantRequest(request)) { - additionalParameters = new HashMap<>(OAuth2EndpointUtils.getParameters(request).toSingleValueMap()); - for (String exclusion : exclusions) { - additionalParameters.remove(exclusion); - } - } - return additionalParameters; - } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationTests.java index b642081d..55063fa3 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationTests.java @@ -15,27 +15,22 @@ */ package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization; -import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.JWSHeader; -import com.nimbusds.jose.JWSSigner; -import com.nimbusds.jose.crypto.MACSigner; -import com.nimbusds.jose.crypto.RSASSASigner; -import com.nimbusds.jose.jwk.JWK; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; + import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.KeyUse; -import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.SecurityContext; -import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.SignedJWT; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.After; import org.junit.AfterClass; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpHeaders; @@ -56,13 +51,11 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; import org.springframework.security.config.test.SpringTestRule; -import org.springframework.security.config.util.ValueCaptureMatcher; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.security.oauth2.core.OAuth2TokenType; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -71,7 +64,12 @@ import org.springframework.security.oauth2.core.oidc.OidcClientRegistration; import org.springframework.security.oauth2.core.oidc.http.converter.OidcClientRegistrationHttpMessageConverter; import org.springframework.security.oauth2.jose.TestJwks; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.JoseHeader; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimsSet; import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.NimbusJwsEncoder; import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; @@ -79,20 +77,13 @@ import org.springframework.security.oauth2.server.authorization.client.JdbcRegis import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; +import org.springframework.security.oauth2.server.authorization.config.ClientSettings; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; - -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.interfaces.RSAPublicKey; -import java.util.Base64; -import java.util.Date; -import java.util.UUID; +import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.containsString; @@ -109,18 +100,16 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * @author Joe Grandja */ public class OidcClientRegistrationTests { - private static final String DEFAULT_ISSUER = "https://auth-server:9000"; private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token"; - private static final String DEFAULT_INTROSPECTION_ENDPOINT_URI = "/oauth2/introspect"; - private static final String DEFAULT_REVOCATION_ENDPOINT_URI = "/oauth2/revoke"; private static final String DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI = "/connect/register"; - private static final String JWT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; private static final HttpMessageConverter accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); private static final HttpMessageConverter clientRegistrationHttpMessageConverter = new OidcClientRegistrationHttpMessageConverter(); private static EmbeddedDatabase db; private static JWKSource jwkSource; + private static JWKSet clientJwkSet; + private static JwtEncoder jwtClientAssertionEncoder; @Rule public final SpringTestRule spring = new SpringTestRule(); @@ -134,10 +123,19 @@ public class OidcClientRegistrationTests { @Autowired private RegisteredClientRepository registeredClientRepository; + @Autowired + private ProviderSettings providerSettings; + + private MockWebServer server; + private String clientJwkSetUrl; + + @BeforeClass public static void init() { JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK); jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); + clientJwkSet = new JWKSet(TestJwks.generateRsaJwk().build()); + jwtClientAssertionEncoder = new NimbusJwsEncoder((jwkSelector, securityContext) -> jwkSelector.select(clientJwkSet)); db = new EmbeddedDatabaseBuilder() .generateUniqueName(true) .setType(EmbeddedDatabaseType.HSQL) @@ -147,8 +145,22 @@ public class OidcClientRegistrationTests { .build(); } + @Before + public void setup() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + this.clientJwkSetUrl = this.server.url("/jwks").toString(); + // @formatter:off + MockResponse response = new MockResponse() + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .setBody(clientJwkSet.toString()); + // @formatter:on + this.server.enqueue(response); + } + @After - public void tearDown() { + public void tearDown() throws Exception { + this.server.shutdown(); jdbcOperations.update("truncate table oauth2_authorization"); jdbcOperations.update("truncate table oauth2_registered_client"); } @@ -247,153 +259,40 @@ public class OidcClientRegistrationTests { assertThat(clientConfigurationResponse.getRegistrationAccessToken()).isNull(); } - @Test - public void whenClientRegisterationWithClientSecretJwtAuthenticationThenJwtClientAuthenticationSuccess() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); - - // @formatter:off - OidcClientRegistration clientRegistration = OidcClientRegistration.builder() - .clientName("client-name") - .redirectUri("https://client.example.com") - .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) - .grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) - .tokenEndpointAuthenticationSigningAlgorithm("HS256") - .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()) - .scope("scope1") - .scope("scope2") - .build(); - // @formatter:on - - OidcClientRegistration clientRegistrationResponse = registerClient(clientRegistration); - ValueCaptureMatcher accessTokenCapture = new ValueCaptureMatcher<>(); - - // token creation with JWT assertion - String clientJwtAssertion = clientSecretJwtAssertion(clientRegistrationResponse, DEFAULT_TOKEN_ENDPOINT_URI); - this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI) - .param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) - .param(OAuth2ParameterNames.SCOPE, "scope1") - .param(OAuth2ParameterNames.CLIENT_ID, clientRegistrationResponse.getClientId()) - .param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_ASSERTION_TYPE) - .param(OAuth2ParameterNames.CLIENT_ASSERTION, clientJwtAssertion)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.access_token").isNotEmpty()) - .andExpect(jsonPath("$.access_token").value(accessTokenCapture)) - .andExpect(jsonPath("$.scope").value("scope1")); - - String accessToken = accessTokenCapture.lastValue(); - - // token introspection with JWT assertion - clientJwtAssertion = clientSecretJwtAssertion(clientRegistrationResponse, DEFAULT_INTROSPECTION_ENDPOINT_URI); - this.mvc.perform(post(DEFAULT_INTROSPECTION_ENDPOINT_URI) - .param(OAuth2ParameterNames.CLIENT_ID, clientRegistrationResponse.getClientId()) - .param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_ASSERTION_TYPE) - .param(OAuth2ParameterNames.CLIENT_ASSERTION, clientJwtAssertion) - .param(OAuth2ParameterNames.TOKEN, accessToken) - .param(OAuth2ParameterNames.TOKEN_TYPE_HINT, OAuth2TokenType.ACCESS_TOKEN.getValue())) - .andExpect(status().isOk()); - - // token revocation with JWT assertion - clientJwtAssertion = clientSecretJwtAssertion(clientRegistrationResponse, DEFAULT_REVOCATION_ENDPOINT_URI); - this.mvc.perform(post(DEFAULT_REVOCATION_ENDPOINT_URI) - .param(OAuth2ParameterNames.CLIENT_ID, clientRegistrationResponse.getClientId()) - .param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_ASSERTION_TYPE) - .param(OAuth2ParameterNames.CLIENT_ASSERTION, clientJwtAssertion) - .param(OAuth2ParameterNames.TOKEN, accessToken) - .param(OAuth2ParameterNames.TOKEN_TYPE_HINT, OAuth2TokenType.ACCESS_TOKEN.getValue())) - .andExpect(status().isOk()); - } - - @Test - public void whenClientRegistrationWithPrivateKeyJwtAuthenticationThenJwtClientAuthenticationSuccess() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); - - KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA"); - gen.initialize(2048); - KeyPair keyPair = gen.generateKeyPair(); - - JWK jwk = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic()) - .keyUse(KeyUse.SIGNATURE) - .keyID(UUID.randomUUID().toString()) - .build(); - - String jwks = "{\"keys\":[" + jwk.toJSONString() + "]}"; - - try (MockWebServer server = new MockWebServer()) { - String jwkSetUrl = server.url("/.well-known/jwks.json").toString(); - - // @formatter:off - OidcClientRegistration clientRegistration = OidcClientRegistration.builder() - .clientName("client-name") - .redirectUri("https://client.example.com") - .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) - .grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) - .tokenEndpointAuthenticationSigningAlgorithm("RS256") - .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()) - .jwkSetUrl(jwkSetUrl) - .scope("scope1") - .scope("scope2") - .build(); - // @formatter:on - - OidcClientRegistration clientRegistrationResponse = registerClient(clientRegistration); - ValueCaptureMatcher accessTokenCapture = new ValueCaptureMatcher<>(); - - // token creation with JWT assertion - String clientJwtAssertion = privateKeyJwtAssertion(keyPair, clientRegistrationResponse, DEFAULT_TOKEN_ENDPOINT_URI); - server.enqueue(new MockResponse().setBody(jwks)); - this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI) - .param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) - .param(OAuth2ParameterNames.SCOPE, "scope1") - .param(OAuth2ParameterNames.CLIENT_ID, clientRegistrationResponse.getClientId()) - .param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_ASSERTION_TYPE) - .param(OAuth2ParameterNames.CLIENT_ASSERTION, clientJwtAssertion)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.access_token").isNotEmpty()) - .andExpect(jsonPath("$.access_token").value(accessTokenCapture)) - .andExpect(jsonPath("$.scope").value("scope1")); - - String accessToken = accessTokenCapture.lastValue(); - - // token introspection with JWT assertion - clientJwtAssertion = privateKeyJwtAssertion(keyPair, clientRegistrationResponse, DEFAULT_INTROSPECTION_ENDPOINT_URI); - server.enqueue(new MockResponse().setBody(jwks)); - this.mvc.perform(post(DEFAULT_INTROSPECTION_ENDPOINT_URI) - .param(OAuth2ParameterNames.CLIENT_ID, clientRegistrationResponse.getClientId()) - .param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_ASSERTION_TYPE) - .param(OAuth2ParameterNames.CLIENT_ASSERTION, clientJwtAssertion) - .param(OAuth2ParameterNames.TOKEN, accessToken) - .param(OAuth2ParameterNames.TOKEN_TYPE_HINT, OAuth2TokenType.ACCESS_TOKEN.getValue())) - .andExpect(status().isOk()); - - // token revocation with JWT assertion - clientJwtAssertion = privateKeyJwtAssertion(keyPair, clientRegistrationResponse, DEFAULT_REVOCATION_ENDPOINT_URI); - server.enqueue(new MockResponse().setBody(jwks)); - this.mvc.perform(post(DEFAULT_REVOCATION_ENDPOINT_URI) - .param(OAuth2ParameterNames.CLIENT_ID, clientRegistrationResponse.getClientId()) - .param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_ASSERTION_TYPE) - .param(OAuth2ParameterNames.CLIENT_ASSERTION, clientJwtAssertion) - .param(OAuth2ParameterNames.TOKEN, accessToken) - .param(OAuth2ParameterNames.TOKEN_TYPE_HINT, OAuth2TokenType.ACCESS_TOKEN.getValue())) - .andExpect(status().isOk()); - - server.shutdown(); - } - } - private OidcClientRegistration registerClient(OidcClientRegistration clientRegistration) throws Exception { // ***** (1) Obtain the "initial" access token used for registering the client String clientRegistrationScope = "client.create"; - RegisteredClient clientRegistrar = TestRegisteredClients.registeredClient2() + // @formatter:off + RegisteredClient clientRegistrar = RegisteredClient.withId("client-registrar-1") + .clientId("client-registrar-1") + .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .scope(clientRegistrationScope) + .clientSettings( + ClientSettings.builder() + .jwkSetUrl(this.clientJwkSetUrl) + .tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS256) + .build() + ) .build(); + // @formatter:on this.registeredClientRepository.save(clientRegistrar); + // @formatter:off + JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256) + .build(); + JwtClaimsSet jwtClaimsSet = jwtClientAssertionClaims(clientRegistrar) + .build(); + // @formatter:on + Jwt jwtAssertion = jwtClientAssertionEncoder.encode(joseHeader, jwtClaimsSet); + MvcResult mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI) .param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) .param(OAuth2ParameterNames.SCOPE, clientRegistrationScope) - .header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth( - clientRegistrar.getClientId(), clientRegistrar.getClientSecret()))) + .param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, "urn:ietf:params:oauth:client-assertion-type:jwt-bearer") + .param(OAuth2ParameterNames.CLIENT_ASSERTION, jwtAssertion.getTokenValue()) + .param(OAuth2ParameterNames.CLIENT_ID, clientRegistrar.getClientId())) .andExpect(status().isOk()) .andExpect(jsonPath("$.access_token").isNotEmpty()) .andExpect(jsonPath("$.scope").value(clientRegistrationScope)) @@ -419,12 +318,19 @@ public class OidcClientRegistrationTests { return readClientRegistrationResponse(mvcResult.getResponse()); } - private static String encodeBasicAuth(String clientId, String secret) throws Exception { - clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name()); - secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name()); - String credentialsString = clientId + ":" + secret; - byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8)); - return new String(encodedBytes, StandardCharsets.UTF_8); + private JwtClaimsSet.Builder jwtClientAssertionClaims(RegisteredClient registeredClient) { + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS); + return JwtClaimsSet.builder() + .issuer(registeredClient.getClientId()) + .subject(registeredClient.getClientId()) + .audience(Collections.singletonList(asUrl(this.providerSettings.getIssuer(), this.providerSettings.getTokenEndpoint()))) + .issuedAt(issuedAt) + .expiresAt(expiresAt); + } + + private static String asUrl(String uri, String path) { + return UriComponentsBuilder.fromUriString(uri).path(path).build().toUriString(); } private static OAuth2AccessTokenResponse readAccessTokenResponse(MockHttpServletResponse response) throws Exception { @@ -445,31 +351,6 @@ public class OidcClientRegistrationTests { return clientRegistrationHttpMessageConverter.read(OidcClientRegistration.class, httpResponse); } - private JWTClaimsSet jwtClientAuthenticationClaims(OidcClientRegistration clientRegistration, String endpointUri) { - return new JWTClaimsSet.Builder() - .subject(clientRegistration.getClientId()) - .issuer(clientRegistration.getClientId()) - .expirationTime(new Date(new Date().getTime() + 60000)) - .audience(DEFAULT_ISSUER + endpointUri) - .build(); - } - - private String clientSecretJwtAssertion(OidcClientRegistration clientRegistration, String endpointUri) throws JOSEException { - JWTClaimsSet claimsSet = jwtClientAuthenticationClaims(clientRegistration, endpointUri); - SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet); - JWSSigner signer = new MACSigner(clientRegistration.getClientSecret().getBytes(StandardCharsets.UTF_8)); - signedJWT.sign(signer); - return signedJWT.serialize(); - } - - private String privateKeyJwtAssertion(KeyPair keyPair, OidcClientRegistration clientRegistration, String endpointUri) throws JOSEException { - JWTClaimsSet claimsSet = jwtClientAuthenticationClaims(clientRegistration, endpointUri); - SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSet); - JWSSigner signer = new RSASSASigner(keyPair.getPrivate()); - signedJWT.sign(signer); - return signedJWT.serialize(); - } - @EnableWebSecurity static class AuthorizationServerConfiguration { @@ -529,7 +410,7 @@ public class OidcClientRegistrationTests { @Bean ProviderSettings providerSettings() { return ProviderSettings.builder() - .issuer(DEFAULT_ISSUER) + .issuer("https://auth-server:9000") .build(); } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/config/util/ValueCaptureMatcher.java b/oauth2-authorization-server/src/test/java/org/springframework/security/config/util/ValueCaptureMatcher.java deleted file mode 100644 index ec248461..00000000 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/config/util/ValueCaptureMatcher.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2020-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.config.util; - -import org.assertj.core.util.Throwables; -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; - -import java.util.ArrayList; -import java.util.List; - -/** - * Hamcrest matcher that records matched values - * - * @author Rafal Lewczuk - * @since 0.2.1 - * @param - */ -public class ValueCaptureMatcher extends BaseMatcher { - - private ClassCastException castException; - private List values = new ArrayList<>(); - - public T lastValue() { - return values.isEmpty() ? null : values.get(values.size()-1); - } - - public List getValues() { - return values; - } - - @Override - public boolean matches(Object item) { - try { - values.add((T) item); - } catch (ClassCastException e) { - castException = e; - return false; - } - return true; - } - - @Override - public void describeTo(Description description) { - if (castException != null) { - description.appendText("ClassCastException with message: "); - description.appendText(castException.getMessage()); - description.appendText(String.format("%n%nStacktrace was: ")); - description.appendText(Throwables.getStackTrace(castException)); - } - } -} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistrationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistrationTests.java index ac3a9095..73c4ef40 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistrationTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistrationTests.java @@ -15,6 +15,7 @@ */ package org.springframework.security.oauth2.core.oidc; +import java.net.URL; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Arrays; @@ -46,8 +47,7 @@ public class OidcClientRegistrationTests { // @formatter:on @Test - public void buildWhenAllClaimsProvidedThenCreated() { - + public void buildWhenAllClaimsProvidedThenCreated() throws Exception { // @formatter:off Instant clientIdIssuedAt = Instant.now(); Instant clientSecretExpiresAt = clientIdIssuedAt.plus(30, ChronoUnit.DAYS); @@ -58,14 +58,14 @@ public class OidcClientRegistrationTests { .clientSecretExpiresAt(clientSecretExpiresAt) .clientName("client-name") .redirectUri("https://client.example.com") - .jwkSetUrl("https://client.example.com/jwks") - .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()) + .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()) .tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256.getName()) .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) .grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) .responseType(OAuth2AuthorizationResponseType.CODE.getValue()) .scope("scope1") .scope("scope2") + .jwkSetUrl("https://client.example.com/jwks") .idTokenSignedResponseAlgorithm(SignatureAlgorithm.RS256.getName()) .registrationAccessToken("registration-access-token") .registrationClientUrl("https://auth-server.com/connect/register?client_id=1") @@ -78,12 +78,13 @@ public class OidcClientRegistrationTests { assertThat(clientRegistration.getClientSecret()).isEqualTo("client-secret"); assertThat(clientRegistration.getClientSecretExpiresAt()).isEqualTo(clientSecretExpiresAt); assertThat(clientRegistration.getClientName()).isEqualTo("client-name"); - assertThat(clientRegistration.getJwkSetUrl().toString()).isEqualTo("https://client.example.com/jwks"); assertThat(clientRegistration.getRedirectUris()).containsOnly("https://client.example.com"); - assertThat(clientRegistration.getTokenEndpointAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()); + assertThat(clientRegistration.getTokenEndpointAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()); + assertThat(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm()).isEqualTo(MacAlgorithm.HS256.getName()); assertThat(clientRegistration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials"); assertThat(clientRegistration.getResponseTypes()).containsOnly("code"); assertThat(clientRegistration.getScopes()).containsExactlyInAnyOrder("scope1", "scope2"); + assertThat(clientRegistration.getJwkSetUrl()).isEqualTo(new URL("https://client.example.com/jwks")); assertThat(clientRegistration.getIdTokenSignedResponseAlgorithm()).isEqualTo("RS256"); assertThat(clientRegistration.getRegistrationAccessToken()).isEqualTo("registration-access-token"); assertThat(clientRegistration.getRegistrationClientUrl().toString()).isEqualTo("https://auth-server.com/connect/register?client_id=1"); @@ -97,7 +98,7 @@ public class OidcClientRegistrationTests { } @Test - public void withClaimsWhenClaimsProvidedThenCreated() { + public void withClaimsWhenClaimsProvidedThenCreated() throws Exception { Instant clientIdIssuedAt = Instant.now(); Instant clientSecretExpiresAt = clientIdIssuedAt.plus(30, ChronoUnit.DAYS); HashMap claims = new HashMap<>(); @@ -107,13 +108,13 @@ public class OidcClientRegistrationTests { claims.put(OidcClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT, clientSecretExpiresAt); claims.put(OidcClientMetadataClaimNames.CLIENT_NAME, "client-name"); claims.put(OidcClientMetadataClaimNames.REDIRECT_URIS, Collections.singletonList("https://client.example.com")); - claims.put(OidcClientMetadataClaimNames.JWKS_URI, "https://client.example.com/jwks"); - claims.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()); + claims.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()); claims.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, MacAlgorithm.HS256.getName()); claims.put(OidcClientMetadataClaimNames.GRANT_TYPES, Arrays.asList( AuthorizationGrantType.AUTHORIZATION_CODE.getValue(), AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())); claims.put(OidcClientMetadataClaimNames.RESPONSE_TYPES, Collections.singletonList("code")); claims.put(OidcClientMetadataClaimNames.SCOPE, Arrays.asList("scope1", "scope2")); + claims.put(OidcClientMetadataClaimNames.JWKS_URI, "https://client.example.com/jwks"); claims.put(OidcClientMetadataClaimNames.ID_TOKEN_SIGNED_RESPONSE_ALG, SignatureAlgorithm.RS256.getName()); claims.put(OidcClientMetadataClaimNames.REGISTRATION_ACCESS_TOKEN, "registration-access-token"); claims.put(OidcClientMetadataClaimNames.REGISTRATION_CLIENT_URI, "https://auth-server.com/connect/register?client_id=1"); @@ -127,12 +128,12 @@ public class OidcClientRegistrationTests { assertThat(clientRegistration.getClientSecretExpiresAt()).isEqualTo(clientSecretExpiresAt); assertThat(clientRegistration.getClientName()).isEqualTo("client-name"); assertThat(clientRegistration.getRedirectUris()).containsOnly("https://client.example.com"); - assertThat(clientRegistration.getJwkSetUrl().toString()).isEqualTo("https://client.example.com/jwks"); - assertThat(clientRegistration.getTokenEndpointAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()); + assertThat(clientRegistration.getTokenEndpointAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()); assertThat(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm()).isEqualTo(MacAlgorithm.HS256.getName()); assertThat(clientRegistration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials"); assertThat(clientRegistration.getResponseTypes()).containsOnly("code"); assertThat(clientRegistration.getScopes()).containsExactlyInAnyOrder("scope1", "scope2"); + assertThat(clientRegistration.getJwkSetUrl()).isEqualTo(new URL("https://client.example.com/jwks")); assertThat(clientRegistration.getIdTokenSignedResponseAlgorithm()).isEqualTo("RS256"); assertThat(clientRegistration.getRegistrationAccessToken()).isEqualTo("registration-access-token"); assertThat(clientRegistration.getRegistrationClientUrl().toString()).isEqualTo("https://auth-server.com/connect/register?client_id=1"); @@ -365,6 +366,16 @@ public class OidcClientRegistrationTests { assertThat(clientRegistration.getScopes()).containsExactly("scope1"); } + @Test + public void buildWhenJwksUriNotUrlThenThrowIllegalArgumentException() { + OidcClientRegistration.Builder builder = this.minimalBuilder + .claim(OidcClientMetadataClaimNames.JWKS_URI, "not an url"); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("jwksUri must be a valid URL"); + } + @Test public void claimWhenNameNullThenThrowIllegalArgumentException() { assertThatIllegalArgumentException() diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcClientRegistrationHttpMessageConverterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcClientRegistrationHttpMessageConverterTests.java index 115f5735..8394545c 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcClientRegistrationHttpMessageConverterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcClientRegistrationHttpMessageConverterTests.java @@ -15,6 +15,7 @@ */ package org.springframework.security.oauth2.core.oidc.http.converter; +import java.net.URL; import java.time.Instant; import java.util.Map; @@ -30,6 +31,7 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; import org.springframework.security.oauth2.core.oidc.OidcClientRegistration; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import static org.assertj.core.api.Assertions.assertThat; @@ -86,7 +88,7 @@ public class OidcClientRegistrationHttpMessageConverterTests { } @Test - public void readInternalWhenValidParametersThenSuccess() { + public void readInternalWhenValidParametersThenSuccess() throws Exception { // @formatter:off String clientRegistrationRequest = "{\n" +" \"client_id\": \"client-id\",\n" @@ -97,7 +99,8 @@ public class OidcClientRegistrationHttpMessageConverterTests { +" \"redirect_uris\": [\n" + " \"https://client.example.com\"\n" + " ],\n" - +" \"token_endpoint_auth_method\": \"client_secret_basic\",\n" + +" \"token_endpoint_auth_method\": \"client_secret_jwt\",\n" + +" \"token_endpoint_auth_signing_alg\": \"HS256\",\n" +" \"grant_types\": [\n" +" \"authorization_code\",\n" +" \"client_credentials\"\n" @@ -106,9 +109,8 @@ public class OidcClientRegistrationHttpMessageConverterTests { +" \"code\"\n" +" ],\n" +" \"scope\": \"scope1 scope2\",\n" + +" \"jwks_uri\": \"https://client.example.com/jwks\",\n" +" \"id_token_signed_response_alg\": \"RS256\",\n" - +" \"jwks_uri\": \"https://client.example.com/jwks\",\n" - +" \"token_endpoint_auth_signing_alg\": \"HS256\",\n" +" \"a-claim\": \"a-value\"\n" +"}\n"; // @formatter:on @@ -123,13 +125,13 @@ public class OidcClientRegistrationHttpMessageConverterTests { assertThat(clientRegistration.getClientSecretExpiresAt()).isEqualTo(Instant.ofEpochSecond(1607637467L)); assertThat(clientRegistration.getClientName()).isEqualTo("client-name"); assertThat(clientRegistration.getRedirectUris()).containsOnly("https://client.example.com"); - assertThat(clientRegistration.getTokenEndpointAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()); + assertThat(clientRegistration.getTokenEndpointAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()); + assertThat(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm()).isEqualTo(MacAlgorithm.HS256.getName()); assertThat(clientRegistration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials"); assertThat(clientRegistration.getResponseTypes()).containsOnly("code"); assertThat(clientRegistration.getScopes()).containsExactlyInAnyOrder("scope1", "scope2"); + assertThat(clientRegistration.getJwkSetUrl()).isEqualTo(new URL("https://client.example.com/jwks")); assertThat(clientRegistration.getIdTokenSignedResponseAlgorithm()).isEqualTo("RS256"); - assertThat(clientRegistration.getJwkSetUrl().toString()).isEqualTo("https://client.example.com/jwks"); - assertThat(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm()).isEqualTo("HS256"); assertThat(clientRegistration.getClaimAsString("a-claim")).isEqualTo("a-value"); } @@ -181,17 +183,17 @@ public class OidcClientRegistrationHttpMessageConverterTests { .clientSecretExpiresAt(Instant.ofEpochSecond(1607637467)) .clientName("client-name") .redirectUri("https://client.example.com") - .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()) + .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()) + .tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256.getName()) .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) .grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) .responseType(OAuth2AuthorizationResponseType.CODE.getValue()) .scope("scope1") .scope("scope2") + .jwkSetUrl("https://client.example.com/jwks") .idTokenSignedResponseAlgorithm(SignatureAlgorithm.RS256.getName()) .registrationAccessToken("registration-access-token") .registrationClientUrl("https://auth-server.com/connect/register?client_id=1") - .jwkSetUrl("https://client.example.com/jwks") - .tokenEndpointAuthenticationSigningAlgorithm("HS256") .claim("a-claim", "a-value") .build(); // @formatter:on @@ -206,15 +208,15 @@ public class OidcClientRegistrationHttpMessageConverterTests { assertThat(clientRegistrationResponse).contains("\"client_secret_expires_at\":1607637467"); assertThat(clientRegistrationResponse).contains("\"client_name\":\"client-name\""); assertThat(clientRegistrationResponse).contains("\"redirect_uris\":[\"https://client.example.com\"]"); - assertThat(clientRegistrationResponse).contains("\"token_endpoint_auth_method\":\"client_secret_basic\""); + assertThat(clientRegistrationResponse).contains("\"token_endpoint_auth_method\":\"client_secret_jwt\""); + assertThat(clientRegistrationResponse).contains("\"token_endpoint_auth_signing_alg\":\"HS256\""); assertThat(clientRegistrationResponse).contains("\"grant_types\":[\"authorization_code\",\"client_credentials\"]"); assertThat(clientRegistrationResponse).contains("\"response_types\":[\"code\"]"); assertThat(clientRegistrationResponse).contains("\"scope\":\"scope1 scope2\""); + assertThat(clientRegistrationResponse).contains("\"jwks_uri\":\"https://client.example.com/jwks\""); assertThat(clientRegistrationResponse).contains("\"id_token_signed_response_alg\":\"RS256\""); assertThat(clientRegistrationResponse).contains("\"registration_access_token\":\"registration-access-token\""); assertThat(clientRegistrationResponse).contains("\"registration_client_uri\":\"https://auth-server.com/connect/register?client_id=1\""); - assertThat(clientRegistrationResponse).contains("\"jwks_uri\":\"https://client.example.com/jwks\""); - assertThat(clientRegistrationResponse).contains("\"token_endpoint_auth_signing_alg\":\"HS256\""); assertThat(clientRegistrationResponse).contains("\"a-claim\":\"a-value\""); } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/TestJwks.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/TestJwks.java index 78e449d3..0a53411b 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/TestJwks.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/TestJwks.java @@ -15,10 +15,13 @@ */ package org.springframework.security.oauth2.jose; +import java.security.KeyPair; +import java.security.KeyPairGenerator; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; +import java.util.UUID; import javax.crypto.SecretKey; @@ -33,6 +36,16 @@ import com.nimbusds.jose.jwk.RSAKey; */ public final class TestJwks { + private static final KeyPairGenerator rsaKeyPairGenerator; + static { + try { + rsaKeyPairGenerator = KeyPairGenerator.getInstance("RSA"); + rsaKeyPairGenerator.initialize(2048); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + // @formatter:off public static final RSAKey DEFAULT_RSA_JWK = jwk( @@ -59,6 +72,16 @@ public final class TestJwks { private TestJwks() { } + public static RSAKey.Builder generateRsaJwk() { + KeyPair keyPair = rsaKeyPairGenerator.generateKeyPair(); + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + // @formatter:off + return jwk(publicKey, privateKey) + .keyID(UUID.randomUUID().toString()); + // @formatter:on + } + public static RSAKey.Builder jwk(RSAPublicKey publicKey, RSAPrivateKey privateKey) { // @formatter:off return new RSAKey.Builder(publicKey) diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProviderTests.java index cf98af1a..5d19403a 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProviderTests.java @@ -15,10 +15,20 @@ */ package org.springframework.security.oauth2.server.authorization.authentication; +import java.nio.charset.StandardCharsets; import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; import java.util.HashMap; import java.util.Map; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.OctetSequenceKey; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; import org.junit.Before; import org.junit.Test; @@ -31,25 +41,35 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2TokenType; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; +import org.springframework.security.oauth2.jose.TestJwks; +import org.springframework.security.oauth2.jose.TestKeys; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.BadJwtException; +import org.springframework.security.oauth2.jwt.JoseHeader; import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtDecoderFactory; +import org.springframework.security.oauth2.jwt.JwtClaimsSet; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.JwtValidationException; +import org.springframework.security.oauth2.jwt.NimbusJwsEncoder; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; +import org.springframework.security.oauth2.server.authorization.config.ClientSettings; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; -import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; /** @@ -80,13 +100,14 @@ public class OAuth2ClientAuthenticationProviderTests { private OAuth2AuthorizationService authorizationService; private OAuth2ClientAuthenticationProvider authenticationProvider; private PasswordEncoder passwordEncoder; - private JwtDecoderFactory jwtDecoderFactory; + private ProviderSettings providerSettings; @Before public void setUp() { this.registeredClientRepository = mock(RegisteredClientRepository.class); this.authorizationService = mock(OAuth2AuthorizationService.class); - this.authenticationProvider = new OAuth2ClientAuthenticationProvider(this.registeredClientRepository, this.authorizationService); + this.authenticationProvider = new OAuth2ClientAuthenticationProvider( + this.registeredClientRepository, this.authorizationService); this.passwordEncoder = spy(new PasswordEncoder() { @Override public String encode(CharSequence rawPassword) { @@ -99,9 +120,8 @@ public class OAuth2ClientAuthenticationProviderTests { } }); this.authenticationProvider.setPasswordEncoder(this.passwordEncoder); - this.authenticationProvider.setProviderSettings(ProviderSettings.builder().issuer("https://auth-server.com").build()); - this.jwtDecoderFactory = mock(JwtDecoderFactory.class); - ReflectionTestUtils.setField(this.authenticationProvider, "jwtDecoderFactory", this.jwtDecoderFactory); + this.providerSettings = ProviderSettings.builder().issuer("https://auth-server.com").build(); + this.authenticationProvider.setProviderSettings(this.providerSettings); } @Test @@ -147,6 +167,23 @@ public class OAuth2ClientAuthenticationProviderTests { }); } + @Test + public void authenticateWhenUnsupportedClientAuthenticationMethodThenThrowOAuth2AuthenticationException() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( + registeredClient.getClientId(), ClientAuthenticationMethod.CLIENT_SECRET_POST, registeredClient.getClientSecret(), null); + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .satisfies(error -> { + assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); + assertThat(error.getDescription()).contains("authentication_method"); + }); + } + @Test public void authenticateWhenInvalidClientSecretThenThrowOAuth2AuthenticationException() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); @@ -220,146 +257,6 @@ public class OAuth2ClientAuthenticationProviderTests { assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient); } - @Test - public void authenticateWhenJwtBearerAndClientNotSupportingItThenThrowOAuth2AuthenticationException() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); - when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) - .thenReturn(registeredClient); - OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( - registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, - registeredClient.getClientSecret(), null); - - assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) - .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) - .extracting("errorCode") - .isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); - - verify(this.jwtDecoderFactory, never()).createDecoder(any()); - } - - @Test - public void authenticateWhenClientJwtAssertionAndPrivateJwtAndFailedCreateDecoderThenThrowOAuth2AuthenticationException() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient() - .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT) - .build(); - when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) - .thenReturn(registeredClient); - when(this.jwtDecoderFactory.createDecoder(any())) - .thenThrow(new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT)); - OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( - "https://auth-server.com", registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, - registeredClient.getClientSecret(), null); - - assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) - .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) - .extracting("errorCode") - .isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); - } - - @Test - public void authenticateWhenClientJwtAssertionAndPrivateKeyJwtAndFailedVerifyTokenThenThrowOAuth2AuthenticationException() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient() - .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT) - .build(); - when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) - .thenReturn(registeredClient); - when(this.jwtDecoderFactory.createDecoder(any())) - .thenReturn(s -> { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); }); - OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( - "https://auth-server.com", registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, - registeredClient.getClientSecret(), null); - - assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) - .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) - .extracting("errorCode") - .isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); - } - - @Test - public void authenticateWhenClientJwtAssertionAndBadAudienceThenThrowOAuth2AuthenticationException() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient() - .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT).build(); - when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) - .thenReturn(registeredClient); - when(this.jwtDecoderFactory.createDecoder(any())) - .thenReturn(s -> createJwtToken("client-1", "https://bad-server.com/oauth2/token")); - OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( - "/oauth2/token", registeredClient.getClientId(), - JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, registeredClient.getClientSecret(), null); - - assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) - .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) - .extracting("errorCode") - .isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); - } - - @Test - public void authenticateWhenClientJwtAssertionAndPrivateJwtVerificationSuccessThenAuthenticate() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient() - .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT).build(); - when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) - .thenReturn(registeredClient); - when(this.jwtDecoderFactory.createDecoder(any())) - .thenReturn(s -> createJwtToken("client-1", "https://auth-server.com/oauth2/token")); - OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( - "https://auth-server.com/oauth2/token", registeredClient.getClientId(), - JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, registeredClient.getClientSecret(), null); - - OAuth2ClientAuthenticationToken authenticationResult = - (OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication); - - assertThat(authenticationResult.isAuthenticated()).isTrue(); - assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId()); - assertThat(authenticationResult.getCredentials().toString()).isEqualTo(registeredClient.getClientSecret()); - assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient); - } - - @Test - public void authenticateWhenClientJwtAssertionAndClientSecretJwtAndFailedVerifyTokenThenThrowOAuth2Exception() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient() - .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT) - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT) - .build(); - when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) - .thenReturn(registeredClient); - when(this.jwtDecoderFactory.createDecoder(any())) - .thenReturn(s -> { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); }); - OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( - "https://auth-server.com", registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, - registeredClient.getClientSecret(), null); - - assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) - .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) - .extracting("errorCode") - .isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); - } - - @Test - public void authenticateWhenClientJwtAssertionAndClientSecretJwtVerificationSuccess() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient() - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT).build(); - when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) - .thenReturn(registeredClient); - when(this.jwtDecoderFactory.createDecoder(any())) - .thenReturn(s -> createJwtToken("client-1", "https://auth-server.com/oauth2/token")); - OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( - "https://auth-server.com/oauth2/token", registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, - registeredClient.getClientSecret(), null); - - OAuth2ClientAuthenticationToken authenticationResult = - (OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication); - - assertThat(authenticationResult.isAuthenticated()).isTrue(); - assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId()); - assertThat(authenticationResult.getCredentials().toString()).isEqualTo(registeredClient.getClientSecret()); - assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient); - } - @Test public void authenticateWhenPkceAndInvalidCodeThenThrowOAuth2AuthenticationException() { RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient().build(); @@ -596,13 +493,34 @@ public class OAuth2ClientAuthenticationProviderTests { } @Test - public void authenticateWhenClientAuthenticationMethodNotConfiguredThenThrowOAuth2AuthenticationException() { + public void authenticateWhenJwtClientAssertionAndInvalidClientIdThenThrowOAuth2AuthenticationException() { + // @formatter:off + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT) + .build(); + // @formatter:on + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( + registeredClient.getClientId() + "-invalid", JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, "jwt-assertion", null); + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .satisfies(error -> { + assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); + assertThat(error.getDescription()).contains(OAuth2ParameterNames.CLIENT_ID); + }); + } + + @Test + public void authenticateWhenJwtClientAssertionAndUnsupportedClientAuthenticationMethodThenThrowOAuth2AuthenticationException() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) .thenReturn(registeredClient); OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( - registeredClient.getClientId(), ClientAuthenticationMethod.CLIENT_SECRET_POST, registeredClient.getClientSecret(), null); + registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, "jwt-assertion", null); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthenticationException.class) .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) @@ -612,6 +530,218 @@ public class OAuth2ClientAuthenticationProviderTests { }); } + @Test + public void authenticateWhenJwtClientAssertionAndMissingJwkSetUrlThenThrowOAuth2AuthenticationException() { + // @formatter:off + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT) + .clientSettings( + ClientSettings.builder() + .tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS256) + .build() + ) + .build(); + // @formatter:on + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( + registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, "jwt-assertion", null); + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .satisfies(error -> { + assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); + assertThat(error.getDescription()).isEqualTo("Failed to find a Signature Verifier for Client: '" + + registeredClient.getId() + "'. Check to ensure you have configured the JWK Set URL."); + }); + } + + @Test + public void authenticateWhenJwtClientAssertionAndMissingClientSecretThenThrowOAuth2AuthenticationException() { + // @formatter:off + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .clientSecret(null) + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT) + .clientSettings( + ClientSettings.builder() + .tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256) + .build() + ) + .build(); + // @formatter:on + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( + registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, "jwt-assertion", null); + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .satisfies(error -> { + assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); + assertThat(error.getDescription()).isEqualTo("Failed to find a Signature Verifier for Client: '" + + registeredClient.getId() + "'. Check to ensure you have configured the client secret."); + }); + } + + @Test + public void authenticateWhenJwtClientAssertionAndMissingSigningAlgorithmThenThrowOAuth2AuthenticationException() { + // @formatter:off + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .clientSecret(TestKeys.DEFAULT_ENCODED_SECRET_KEY) + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT) + .build(); + // @formatter:on + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( + registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, "jwt-assertion", null); + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .satisfies(error -> { + assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); + assertThat(error.getDescription()).isEqualTo("Failed to find a Signature Verifier for Client: '" + + registeredClient.getId() + "'. Check to ensure you have configured a valid JWS Algorithm: 'null'."); + }); + } + + @Test + public void authenticateWhenJwtClientAssertionAndInvalidCredentialsThenThrowOAuth2AuthenticationException() { + // @formatter:off + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .clientSecret(TestKeys.DEFAULT_ENCODED_SECRET_KEY) + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT) + .clientSettings( + ClientSettings.builder() + .tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256) + .build() + ) + .build(); + // @formatter:on + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( + registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, "invalid-jwt-assertion", null); + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasCauseInstanceOf(BadJwtException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .satisfies(error -> { + assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); + assertThat(error.getDescription()).contains(OAuth2ParameterNames.CLIENT_ASSERTION); + }); + } + + @Test + public void authenticateWhenJwtClientAssertionAndInvalidClaimsThenThrowOAuth2AuthenticationException() { + // @formatter:off + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .clientSecret(TestKeys.DEFAULT_ENCODED_SECRET_KEY) + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT) + .clientSettings( + ClientSettings.builder() + .tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256) + .build() + ) + .build(); + // @formatter:on + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + // @formatter:off + JoseHeader joseHeader = JoseHeader.withAlgorithm(MacAlgorithm.HS256) + .build(); + JwtClaimsSet jwtClaimsSet = JwtClaimsSet.builder() + .issuer("invalid-iss") + .subject("invalid-sub") + .audience(Collections.singletonList("invalid-aud")) + .build(); + // @formatter:on + + JwtEncoder jwsEncoder = createEncoder(TestKeys.DEFAULT_ENCODED_SECRET_KEY, "HmacSHA256"); + Jwt jwtAssertion = jwsEncoder.encode(joseHeader, jwtClaimsSet); + + OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( + registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, jwtAssertion.getTokenValue(), null); + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasCauseInstanceOf(JwtValidationException.class) + .extracting(ex -> (OAuth2AuthenticationException) ex) + .satisfies(ex -> { + assertThat(ex.getError().getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); + assertThat(ex.getError().getDescription()).contains(OAuth2ParameterNames.CLIENT_ASSERTION); + JwtValidationException jwtValidationException = (JwtValidationException) ex.getCause(); + assertThat(jwtValidationException.getErrors()).hasSize(4); // iss, sub, aud, exp + }); + } + + @Test + public void authenticateWhenJwtClientAssertionAndValidCredentialsThenAuthenticated() { + // @formatter:off + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .clientSecret(TestKeys.DEFAULT_ENCODED_SECRET_KEY) + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT) + .clientSettings( + ClientSettings.builder() + .tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256) + .build() + ) + .build(); + // @formatter:on + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + // @formatter:off + JoseHeader joseHeader = JoseHeader.withAlgorithm(MacAlgorithm.HS256) + .build(); + JwtClaimsSet jwtClaimsSet = jwtClientAssertionClaims(registeredClient) + .build(); + // @formatter:on + + JwtEncoder jwsEncoder = createEncoder(TestKeys.DEFAULT_ENCODED_SECRET_KEY, "HmacSHA256"); + Jwt jwtAssertion = jwsEncoder.encode(joseHeader, jwtClaimsSet); + + OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( + registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, jwtAssertion.getTokenValue(), null); + OAuth2ClientAuthenticationToken authenticationResult = + (OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication); + + verifyNoInteractions(this.passwordEncoder); + + assertThat(authenticationResult.isAuthenticated()).isTrue(); + assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId()); + assertThat(authenticationResult.getCredentials()).isInstanceOf(Jwt.class); + assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient); + assertThat(authenticationResult.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_JWT); + } + + private JwtClaimsSet.Builder jwtClientAssertionClaims(RegisteredClient registeredClient) { + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS); + return JwtClaimsSet.builder() + .issuer(registeredClient.getClientId()) + .subject(registeredClient.getClientId()) + .audience(Collections.singletonList(asUrl(this.providerSettings.getIssuer(), this.providerSettings.getTokenEndpoint()))) + .issuedAt(issuedAt) + .expiresAt(expiresAt); + } + + private static JwtEncoder createEncoder(String secret, String algorithm) { + SecretKey secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), algorithm); + OctetSequenceKey secretKeyJwk = TestJwks.jwk(secretKey).build(); + JWKSource jwkSource = (jwkSelector, securityContext) -> + jwkSelector.select(new JWKSet(secretKeyJwk)); + return new NimbusJwsEncoder(jwkSource); + } + + private static String asUrl(String uri, String path) { + return UriComponentsBuilder.fromUriString(uri).path(path).build().toUriString(); + } + private static Map createAuthorizationCodeTokenParameters() { Map parameters = new HashMap<>(); parameters.put(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); @@ -639,12 +769,4 @@ public class OAuth2ClientAuthenticationProviderTests { return parameters; } - private static Jwt createJwtToken(String subject, String audience) { - Map headers = new HashMap<>(); - headers.put("kid", "123"); - Map claims = new HashMap<>(); - claims.put("sub", subject); - claims.put("aud", audience); - return new Jwt("123", Instant.now().minusSeconds(30), Instant.now().plusSeconds(30), headers, claims); - } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationTokenTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationTokenTests.java index 3c8c8dd2..22b30c2d 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationTokenTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationTokenTests.java @@ -32,23 +32,16 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; */ public class OAuth2ClientAuthenticationTokenTests { - @Test - public void constructorWhenRequestUriNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken(null, "clientId", ClientAuthenticationMethod.CLIENT_SECRET_BASIC, "secret", null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("requestUri cannot be empty"); - } - @Test public void constructorWhenClientIdNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken("issuer", null, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, "secret", null)) + assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken(null, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, "secret", null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("clientId cannot be empty"); } @Test public void constructorWhenClientAuthenticationMethodNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken("issuer", "clientId", null, "clientSecret", null)) + assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken("clientId", null, "clientSecret", null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("clientAuthenticationMethod cannot be null"); } @@ -62,10 +55,9 @@ public class OAuth2ClientAuthenticationTokenTests { @Test public void constructorWhenClientCredentialsProvidedThenCreated() { - OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken("issuer", "clientId", + OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken("clientId", ClientAuthenticationMethod.CLIENT_SECRET_BASIC, "secret", null); assertThat(authentication.isAuthenticated()).isFalse(); - assertThat(authentication.getRequestUri()).isEqualTo("issuer"); assertThat(authentication.getPrincipal().toString()).isEqualTo("clientId"); assertThat(authentication.getCredentials()).isEqualTo("secret"); assertThat(authentication.getRegisteredClient()).isNull(); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/RegisteredClientJwtAssertionDecoderFactoryTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/RegisteredClientJwtAssertionDecoderFactoryTests.java deleted file mode 100644 index 5f696f73..00000000 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/RegisteredClientJwtAssertionDecoderFactoryTests.java +++ /dev/null @@ -1,436 +0,0 @@ -/* - * Copyright 2020-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.oauth2.server.authorization.authentication; - -import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.JWSHeader; -import com.nimbusds.jose.JWSSigner; -import com.nimbusds.jose.crypto.MACSigner; -import com.nimbusds.jose.crypto.RSASSASigner; -import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.KeyUse; -import com.nimbusds.jose.jwk.RSAKey; -import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.SignedJWT; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.Before; -import org.junit.Test; -import org.springframework.security.oauth2.core.ClientAuthenticationMethod; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.jose.jws.MacAlgorithm; -import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.JwtDecoderFactory; -import org.springframework.security.oauth2.jwt.JwtException; -import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; -import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; -import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; -import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; -import org.springframework.security.oauth2.server.authorization.config.ClientSettings; -import org.springframework.test.util.ReflectionTestUtils; - -import java.nio.charset.StandardCharsets; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.interfaces.RSAPublicKey; -import java.time.Instant; -import java.util.Date; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link OAuth2ClientAuthenticationProvider.RegisteredClientJwtAssertionDecoderFactory} - * - * @author Rafal Lewczuk - */ -public class RegisteredClientJwtAssertionDecoderFactoryTests { - - private JwtDecoderFactory registeredClientDecoderFactory; - - @Before - public void setUp() { - OAuth2ClientAuthenticationProvider authenticationProvider = new OAuth2ClientAuthenticationProvider( - mock(RegisteredClientRepository.class), mock(OAuth2AuthorizationService.class)); - this.registeredClientDecoderFactory = (JwtDecoderFactory) - ReflectionTestUtils.getField(authenticationProvider, "jwtDecoderFactory"); - } - - @Test - public void createDecoderWhenRegisteredClientNullThenThrowIllegalArgumentException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> registeredClientDecoderFactory.createDecoder(null)) - .withMessage("registeredClient cannot be null"); - } - - @Test - public void createDecoderWhenClientAuthenticationMethodNotSupportedThenThrowOAuth2AuthenticationException() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); - - assertThatThrownBy(() -> this.registeredClientDecoderFactory.createDecoder(registeredClient)) - .isInstanceOf(OAuth2AuthenticationException.class); - } - - @Test - public void createDecoderWithClientSecretJwtWhenClientSecretNullThenThrowOAuth2Exception() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient() - .clientSecret(null) - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT) - .clientSettings(ClientSettings.builder().tokenEndpointSigningAlgorithm(MacAlgorithm.HS256).build()) - .build(); - - assertThatThrownBy(() -> this.registeredClientDecoderFactory.createDecoder(registeredClient)) - .isInstanceOf(OAuth2AuthenticationException.class); - } - - @Test - public void createDecoderWithClientSecretJwtClientThenReturnDecoder() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient() - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT) - .clientSecret("0123456789abcdef0123456789ABCDEF") - .clientSettings(ClientSettings.builder().tokenEndpointSigningAlgorithm(MacAlgorithm.HS256).build()) - .build(); - - JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient); - - assertThat(jwtDecoder).isNotNull(); - } - - @Test - public void createDecoderWithClientSecretJwtTwiceThenReturnCachedDecoder() { - RegisteredClient.Builder registeredClientBuilder = TestRegisteredClients.registeredClient() - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT) - .clientSecret("0123456789abcdef0123456789ABCDEF") - .clientSettings(ClientSettings.builder().tokenEndpointSigningAlgorithm(MacAlgorithm.HS256).build()); - - JwtDecoder decoder1 = this.registeredClientDecoderFactory.createDecoder(registeredClientBuilder.build()); - JwtDecoder decoder2 = this.registeredClientDecoderFactory.createDecoder(registeredClientBuilder.build()); - - assertThat(decoder1).isNotNull(); - assertThat(decoder2).isSameAs(decoder1); - } - - @Test - public void createDecoderWithClientSecretJwtAndSecondWithChangedAlgorithmThenReturnRecreatedDecoder() { - RegisteredClient.Builder registeredClientBuilder = TestRegisteredClients.registeredClient() - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT) - .clientSecret("0123456789abcdef0123456789ABCDEF"); - RegisteredClient registeredClient1 = registeredClientBuilder.clientSettings( - ClientSettings.builder().tokenEndpointSigningAlgorithm(MacAlgorithm.HS256).build()).build(); - RegisteredClient registeredClient = registeredClientBuilder.clientSettings( - ClientSettings.builder().tokenEndpointSigningAlgorithm(MacAlgorithm.HS512).build()).build(); - - JwtDecoder decoder1 = this.registeredClientDecoderFactory.createDecoder(registeredClient1); - JwtDecoder decoder2 = this.registeredClientDecoderFactory.createDecoder(registeredClient); - - assertThat(decoder1).isNotNull(); - assertThat(decoder2).isNotNull(); - assertThat(decoder1).isNotSameAs(decoder2); - } - - @Test - public void createDecoderWithClientSecretJwtAndSecondWithChangedSecretThenReturnRecreatedDecoder() { - RegisteredClient.Builder registeredClientBuilder = TestRegisteredClients.registeredClient() - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT) - .clientSettings(ClientSettings.builder().tokenEndpointSigningAlgorithm(MacAlgorithm.HS256).build()); - RegisteredClient registeredClient1 = registeredClientBuilder.clientSecret("0123456789abcdef0123456789ABCDEF").build(); - RegisteredClient registeredClient2 = registeredClientBuilder.clientSecret("0123456789ABCDEF0123456789abcdef").build(); - - JwtDecoder decoder1 = this.registeredClientDecoderFactory.createDecoder(registeredClient1); - JwtDecoder decoder2 = this.registeredClientDecoderFactory.createDecoder(registeredClient2); - - assertThat(decoder1).isNotNull(); - assertThat(decoder2).isNotNull(); - assertThat(decoder1).isNotSameAs(decoder2); - } - - @Test - public void createDecoderWithPrivateKeyJwtMissingJwksUrlThenThrowOAuth2AuthenticationException() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient() - .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT) - .clientSettings(ClientSettings.builder() - .tokenEndpointSigningAlgorithm(SignatureAlgorithm.RS256).build()) - .build(); - - assertThatThrownBy(() -> this.registeredClientDecoderFactory.createDecoder(registeredClient)) - .isInstanceOf(OAuth2AuthenticationException.class); - } - - @Test - public void createDecoderWithPrivateKeyJwtThenReturnDecoder() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient() - .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT) - .clientSettings(ClientSettings.builder() - .tokenEndpointSigningAlgorithm(SignatureAlgorithm.RS256).jwkSetUrl("https://client.example.com/jwks").build()) - .build(); - - JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient); - - assertThat(jwtDecoder).isNotNull(); - } - - @Test - public void createDecoderWithPrivateKeyJwtAndSecondWithChangedAlgorithmThenReturnRecreatedDecoder() { - RegisteredClient.Builder registeredClientBuilder = TestRegisteredClients.registeredClient() - .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT); - - RegisteredClient registeredClient1 = registeredClientBuilder.clientSettings( - ClientSettings.builder().tokenEndpointSigningAlgorithm(SignatureAlgorithm.RS256) - .jwkSetUrl("https://keysite.com/jwks").build()).build(); - RegisteredClient registeredClient2 = registeredClientBuilder.clientSettings( - ClientSettings.builder().tokenEndpointSigningAlgorithm(SignatureAlgorithm.RS512) - .jwkSetUrl("https://keysite.com/jwks").build()).build(); - - JwtDecoder decoder1 = this.registeredClientDecoderFactory.createDecoder(registeredClient1); - JwtDecoder decoder2 = this.registeredClientDecoderFactory.createDecoder(registeredClient2); - - assertThat(decoder1).isNotNull(); - assertThat(decoder2).isNotNull(); - assertThat(decoder1).isNotSameAs(decoder2); - } - - @Test - public void createDecoderWithPrivateKeyJwtAndSecondWithChangedJwksUrlThenReturnRecreatedDecoder() { - RegisteredClient.Builder registeredClientBuilder = TestRegisteredClients.registeredClient() - .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT); - - RegisteredClient client1 = registeredClientBuilder.clientSettings( - ClientSettings.builder().tokenEndpointSigningAlgorithm(SignatureAlgorithm.RS256) - .jwkSetUrl("https://keysite1.com/jwks").build()).build(); - RegisteredClient client2 = registeredClientBuilder.clientSettings( - ClientSettings.builder().tokenEndpointSigningAlgorithm(SignatureAlgorithm.RS256) - .jwkSetUrl("https://keysite2.com/jwks").build()).build(); - - OAuth2ClientAuthenticationToken token = new OAuth2ClientAuthenticationToken( - "https://auth-server/oauth2/token", "client-1", ClientAuthenticationMethod.CLIENT_SECRET_JWT, "jwt", null); - - JwtDecoder decoder1 = this.registeredClientDecoderFactory.createDecoder(client1); - JwtDecoder decoder2 = this.registeredClientDecoderFactory.createDecoder(client2); - - assertThat(decoder1).isNotNull(); - assertThat(decoder2).isNotNull(); - assertThat(decoder1).isNotSameAs(decoder2); - } - - @Test - public void createDecoderWithPrivateKeyJwtNullAlgorithmThenReturnDefaultRS256Decoder() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient() - .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT) - .clientSettings( - ClientSettings.builder() - .jwkSetUrl("https://keysite1.com/jwks") - .build()) - .build(); - - JwtDecoder decoder = this.registeredClientDecoderFactory.createDecoder(registeredClient); - assertThat(decoder).isNotNull(); - } - - @Test - public void validateClientSecretJwtTokenWhenValidThenReturnJwtObject() throws Exception { - RegisteredClient registeredClient = defaultRegisteredClient(); - JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient); - String clientJwtAssertion = clientSecretJwtAssertion(registeredClient, - new JWTClaimsSet.Builder() - .issuer(registeredClient.getClientId()) - .subject(registeredClient.getClientId()) - .expirationTime(Date.from(Instant.now())) - .build()); - - assertThat(jwtDecoder.decode(clientJwtAssertion)).isNotNull(); - } - - @Test - public void validateClientSecretJwtTokenWhenBadIssuerThenThrowJwtException() throws Exception { - RegisteredClient registeredClient = defaultRegisteredClient(); - JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient); - String clientJwtAssertion = clientSecretJwtAssertion(registeredClient, - new JWTClaimsSet.Builder() - .issuer("bad-issuer") - .subject(registeredClient.getClientId()) - .expirationTime(Date.from(Instant.now())) - .build()); - - assertThatThrownBy(() -> jwtDecoder.decode(clientJwtAssertion)) - .isInstanceOf(JwtException.class) - .extracting("message") - .matches(s -> s.toString().contains("The iss claim is not valid")); - } - - @Test - public void validateClientSecretJwtTokenWhenBadSubjectThenThrowJwtException() throws Exception { - RegisteredClient registeredClient = defaultRegisteredClient(); - JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient); - String clientJwtAssertion = clientSecretJwtAssertion(registeredClient, - new JWTClaimsSet.Builder() - .issuer(registeredClient.getClientId()) - .subject("bad-client") - .expirationTime(Date.from(Instant.now())) - .build()); - - assertThatThrownBy(() -> jwtDecoder.decode(clientJwtAssertion)) - .isInstanceOf(JwtException.class) - .extracting("message") - .matches(s -> s.toString().contains("The sub claim is not valid")); - } - - @Test - public void validateClientSecretJwtTokenWhenNoExpClaimThenThrowJwtException() throws Exception { - RegisteredClient registeredClient = defaultRegisteredClient(); - JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient); - String clientJwtAssertion = clientSecretJwtAssertion(registeredClient, - new JWTClaimsSet.Builder() - .subject(registeredClient.getClientId()) - .issuer(registeredClient.getClientId()) - .build()); - - assertThatThrownBy(() -> jwtDecoder.decode(clientJwtAssertion)) - .isInstanceOf(JwtException.class) - .extracting("message") - .matches(s -> s.toString().contains("The exp claim is not valid")); - } - - @Test - public void validateClientSecretJwtTokenWhenExpiredThenThrowJwtException() throws Exception { - RegisteredClient registeredClient = defaultRegisteredClient(); - JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient); - String clientJwtAssertion = clientSecretJwtAssertion(registeredClient, - new JWTClaimsSet.Builder() - .subject(registeredClient.getClientId()) - .issuer(registeredClient.getClientId()) - .expirationTime(Date.from(Instant.now().minusSeconds(240))) - .build()); - - assertThatThrownBy(() -> jwtDecoder.decode(clientJwtAssertion)) - .isInstanceOf(JwtException.class) - .extracting("message") - .matches(s -> s.toString().contains("Jwt expired at")); - } - - @Test - public void validateClientSecretJwtTokenWhenExpiredWithinSkewThenReturnJwtObject() throws Exception { - RegisteredClient registeredClient = defaultRegisteredClient(); - JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient); - String clientJwtAssertion = clientSecretJwtAssertion(registeredClient, - new JWTClaimsSet.Builder() - .subject(registeredClient.getClientId()) - .issuer(registeredClient.getClientId()) - .expirationTime(Date.from(Instant.now().minusSeconds(30))) - .build()); - - assertThat(jwtDecoder.decode(clientJwtAssertion)).isNotNull(); - } - - @Test - public void validateClientSecretJwtTokenWhenInvalidNbfThenThrowJwtException() throws Exception { - RegisteredClient registeredClient = defaultRegisteredClient(); - JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient); - String clientJwtAssertion = clientSecretJwtAssertion(registeredClient, - new JWTClaimsSet.Builder() - .subject(registeredClient.getClientId()) - .issuer(registeredClient.getClientId()) - .expirationTime(Date.from(Instant.now())) - .notBeforeTime(Date.from(Instant.now().plusSeconds(90))) - .build()); - - assertThatThrownBy(() -> jwtDecoder.decode(clientJwtAssertion)) - .isInstanceOf(JwtException.class) - .extracting("message") - .matches(s -> s.toString().contains("Jwt used before")); - } - - @Test - public void validateClientSecretJwtTokenWhenInvalidIatThenThrowJwtException() throws Exception { - RegisteredClient registeredClient = defaultRegisteredClient(); - JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient); - String clientJwtAssertion = clientSecretJwtAssertion(registeredClient, - new JWTClaimsSet.Builder() - .subject(registeredClient.getClientId()) - .issuer(registeredClient.getClientId()) - .expirationTime(Date.from(Instant.now())) - .issueTime(Date.from(Instant.now().plusSeconds(90))) - .build()); - - assertThatThrownBy(() -> jwtDecoder.decode(clientJwtAssertion)) - .isInstanceOf(JwtException.class) - .extracting("message") - .matches(s -> s.toString().contains("expiresAt must be after issuedAt")); - } - - @Test - public void validatePrivateKeyJwtTokenWhenValidThenReturnJwtObject() throws Exception { - KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA"); - gen.initialize(2048); - KeyPair keyPair = gen.generateKeyPair(); - - JWK jwk = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic()) - .keyUse(KeyUse.SIGNATURE) - .keyID(UUID.randomUUID().toString()) - .build(); - - String jwks = "{\"keys\":[" + jwk.toJSONString() + "]}"; - - try (MockWebServer server = new MockWebServer()) { - String jwkSetUrl = server.url("/.well-known/jwks.json").toString(); - server.enqueue(new MockResponse().setBody(jwks)); - - RegisteredClient registeredClient = TestRegisteredClients.registeredClient() - .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT) - .clientSettings(ClientSettings.builder() - .tokenEndpointSigningAlgorithm(SignatureAlgorithm.RS256) - .jwkSetUrl(jwkSetUrl).build()) - .build(); - - JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient); - - JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() - .issuer(registeredClient.getClientId()) - .subject(registeredClient.getClientId()) - .expirationTime(Date.from(Instant.now())) - .build(); - SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSet); - JWSSigner signer = new RSASSASigner(keyPair.getPrivate()); - signedJWT.sign(signer); - String clientJwtAssertion = signedJWT.serialize(); - - assertThat(jwtDecoder.decode(clientJwtAssertion)).isNotNull(); - - server.shutdown(); - } - - } - - private RegisteredClient defaultRegisteredClient() { - return TestRegisteredClients.registeredClient() - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT) - .clientSecret("0123456789abcdef0123456789ABCDEF") - .clientSettings(ClientSettings.builder().tokenEndpointSigningAlgorithm(MacAlgorithm.HS256).build()) - .build(); - } - - private String clientSecretJwtAssertion(RegisteredClient registeredClient, JWTClaimsSet claimsSet) throws JOSEException { - SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet); - JWSSigner signer = new MACSigner(registeredClient.getClientSecret().getBytes(StandardCharsets.UTF_8)); - signedJWT.sign(signer); - String clientJwtAssertion = signedJWT.serialize(); - return clientJwtAssertion; - } -} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepositoryTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepositoryTests.java index 2e8da96f..f385a627 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepositoryTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepositoryTests.java @@ -143,17 +143,10 @@ public class JdbcRegisteredClientRepositoryTests { @Test public void saveWhenNewThenSaved() { - RegisteredClient expectedRegisteredClient = TestRegisteredClients.registeredClient().build(); - this.registeredClientRepository.save(expectedRegisteredClient); - RegisteredClient registeredClient = this.registeredClientRepository.findById(expectedRegisteredClient.getId()); - assertThat(registeredClient).isEqualTo(expectedRegisteredClient); - } - - @Test - public void saveWhenCustomTokenEndpointSigningAlgorithmsThenSaved() { RegisteredClient expectedRegisteredClient = TestRegisteredClients.registeredClient() .clientSettings(ClientSettings.builder() - .tokenEndpointSigningAlgorithm(MacAlgorithm.HS256).build()) + .tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256).build() + ) .build(); this.registeredClientRepository.save(expectedRegisteredClient); RegisteredClient registeredClient = this.registeredClientRepository.findById(expectedRegisteredClient.getId()); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ClientSettingsTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ClientSettingsTests.java index 9b9356d8..7d0fe74f 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ClientSettingsTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ClientSettingsTests.java @@ -16,8 +16,8 @@ package org.springframework.security.oauth2.server.authorization.config; import org.junit.Test; + import org.springframework.security.oauth2.jose.jws.MacAlgorithm; -import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import static org.assertj.core.api.Assertions.assertThat; @@ -31,10 +31,9 @@ public class ClientSettingsTests { @Test public void buildWhenDefaultThenDefaultsAreSet() { ClientSettings clientSettings = ClientSettings.builder().build(); - assertThat(clientSettings.getSettings()).hasSize(3); + assertThat(clientSettings.getSettings()).hasSize(2); assertThat(clientSettings.isRequireProofKey()).isFalse(); assertThat(clientSettings.isRequireAuthorizationConsent()).isFalse(); - assertThat(clientSettings.getTokenEndpointSigningAlgorithm()).isEqualTo(SignatureAlgorithm.RS256); } @Test @@ -54,19 +53,19 @@ public class ClientSettingsTests { } @Test - public void tokenEndpointAlgorithmWhenHS256ThenSet() { + public void tokenEndpointAuthenticationSigningAlgorithmWhenHS256ThenSet() { ClientSettings clientSettings = ClientSettings.builder() - .tokenEndpointSigningAlgorithm(MacAlgorithm.HS256) + .tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256) .build(); - assertThat(clientSettings.getTokenEndpointSigningAlgorithm()).isEqualTo(MacAlgorithm.HS256); + assertThat(clientSettings.getTokenEndpointAuthenticationSigningAlgorithm()).isEqualTo(MacAlgorithm.HS256); } @Test - public void whenJwkSetUrlSetThenSet() { + public void jwkSetUrlWhenProvidedThenSet() { ClientSettings clientSettings = ClientSettings.builder() - .jwkSetUrl("https://auth-server:9000/jwks") + .jwkSetUrl("https://client.example.com/jwks") .build(); - assertThat(clientSettings.getJwkSetUrl()).isEqualTo("https://auth-server:9000/jwks"); + assertThat(clientSettings.getJwkSetUrl()).isEqualTo("https://client.example.com/jwks"); } @Test @@ -75,7 +74,7 @@ public class ClientSettingsTests { .setting("name1", "value1") .settings(settings -> settings.put("name2", "value2")) .build(); - assertThat(clientSettings.getSettings()).hasSize(5); + assertThat(clientSettings.getSettings()).hasSize(4); assertThat(clientSettings.getSetting("name1")).isEqualTo("value1"); assertThat(clientSettings.getSetting("name2")).isEqualTo("value2"); } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java index 758e6038..6ac02163 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java @@ -37,6 +37,7 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2TokenType; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.oidc.OidcClientMetadataClaimNames; import org.springframework.security.oauth2.core.oidc.OidcClientRegistration; import org.springframework.security.oauth2.jose.jws.MacAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; @@ -52,6 +53,7 @@ import org.springframework.security.oauth2.server.authorization.TestOAuth2Author import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; +import org.springframework.security.oauth2.server.authorization.config.ClientSettings; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.web.util.UriComponentsBuilder; @@ -286,8 +288,11 @@ public class OidcClientRegistrationAuthenticationProviderTests { assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode") - .isEqualTo("invalid_redirect_uri"); + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .satisfies(error -> { + assertThat(error.getErrorCode()).isEqualTo("invalid_redirect_uri"); + assertThat(error.getDescription()).contains(OidcClientMetadataClaimNames.REDIRECT_URIS); + }); verify(this.authorizationService).findByToken( eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)); } @@ -318,12 +323,139 @@ public class OidcClientRegistrationAuthenticationProviderTests { assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode") - .isEqualTo("invalid_redirect_uri"); + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .satisfies(error -> { + assertThat(error.getErrorCode()).isEqualTo("invalid_redirect_uri"); + assertThat(error.getDescription()).contains(OidcClientMetadataClaimNames.REDIRECT_URIS); + }); verify(this.authorizationService).findByToken( eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)); } + @Test + public void authenticateWhenClientRegistrationRequestAndInvalidTokenEndpointAuthenticationMethodThenThrowOAuth2AuthenticationException() { + Jwt jwt = createJwtClientRegistration(); + OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + jwt.getTokenValue(), jwt.getIssuedAt(), + jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE)); + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization( + registeredClient, jwtAccessToken, jwt.getClaims()).build(); + when(this.authorizationService.findByToken( + eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN))) + .thenReturn(authorization); + + JwtAuthenticationToken principal = new JwtAuthenticationToken( + jwt, AuthorityUtils.createAuthorityList("SCOPE_client.create")); + // @formatter:off + OidcClientRegistration.Builder builder = OidcClientRegistration.builder() + .redirectUri("https://client.example.com"); + // @formatter:on + + String invalidClientMetadataErrorCode = "invalid_client_metadata"; + + // @formatter:off + builder + .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()) + .tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256.getName()); + assertWhenClientRegistrationRequestInvalidThenThrowOAuth2AuthenticationException(principal, builder.build(), + invalidClientMetadataErrorCode, OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD); + // @formatter:on + + // @formatter:off + builder + .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()) + .tokenEndpointAuthenticationSigningAlgorithm("none"); + assertWhenClientRegistrationRequestInvalidThenThrowOAuth2AuthenticationException(principal, builder.build(), + invalidClientMetadataErrorCode, OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD); + // @formatter:on + + // @formatter:off + builder + .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()) + .tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS256.getName()); + assertWhenClientRegistrationRequestInvalidThenThrowOAuth2AuthenticationException(principal, builder.build(), + invalidClientMetadataErrorCode, OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD); + // @formatter:on + + // @formatter:off + builder + .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()) + .jwkSetUrl("https://client.example.com/jwks") + .tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256.getName()); + assertWhenClientRegistrationRequestInvalidThenThrowOAuth2AuthenticationException(principal, builder.build(), + invalidClientMetadataErrorCode, OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD); + // @formatter:on + + // @formatter:off + builder + .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()) + .tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS256.getName()); + assertWhenClientRegistrationRequestInvalidThenThrowOAuth2AuthenticationException(principal, builder.build(), + invalidClientMetadataErrorCode, OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD); + // @formatter:on + } + + private void assertWhenClientRegistrationRequestInvalidThenThrowOAuth2AuthenticationException( + Authentication principal, OidcClientRegistration clientRegistration, String errorCode, String errorDescription) { + + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken( + principal, clientRegistration); + + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .satisfies(error -> { + assertThat(error.getErrorCode()).isEqualTo(errorCode); + assertThat(error.getDescription()).contains(errorDescription); + }); + } + + @Test + public void authenticateWhenClientRegistrationRequestAndTokenEndpointAuthenticationSigningAlgorithmNotProvidedThenDefaults() { + Jwt jwt = createJwtClientRegistration(); + OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + jwt.getTokenValue(), jwt.getIssuedAt(), + jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE)); + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization( + registeredClient, jwtAccessToken, jwt.getClaims()).build(); + when(this.authorizationService.findByToken( + eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN))) + .thenReturn(authorization); + when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwtClientConfiguration()); + + JwtAuthenticationToken principal = new JwtAuthenticationToken( + jwt, AuthorityUtils.createAuthorityList("SCOPE_client.create")); + // @formatter:off + OidcClientRegistration.Builder builder = OidcClientRegistration.builder() + .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) + .redirectUri("https://client.example.com") + .scope("scope1"); + // @formatter:on + + // @formatter:off + builder + .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()); + // @formatter:on + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken( + principal, builder.build()); + OidcClientRegistrationAuthenticationToken authenticationResult = + (OidcClientRegistrationAuthenticationToken) this.authenticationProvider.authenticate(authentication); + assertThat(authenticationResult.getClientRegistration().getTokenEndpointAuthenticationSigningAlgorithm()) + .isEqualTo(MacAlgorithm.HS256.getName()); + + // @formatter:off + builder + .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()) + .jwkSetUrl("https://client.example.com/jwks"); + // @formatter:on + authentication = new OidcClientRegistrationAuthenticationToken(principal, builder.build()); + authenticationResult = (OidcClientRegistrationAuthenticationToken) this.authenticationProvider.authenticate(authentication); + assertThat(authenticationResult.getClientRegistration().getTokenEndpointAuthenticationSigningAlgorithm()) + .isEqualTo(SignatureAlgorithm.RS256.getName()); + } + @Test public void authenticateWhenClientRegistrationRequestAndValidAccessTokenThenReturnClientRegistration() { Jwt jwt = createJwtClientRegistration(); @@ -425,165 +557,6 @@ public class OidcClientRegistrationAuthenticationProviderTests { assertThat(clientRegistrationResult.getRegistrationAccessToken()).isEqualTo(jwt.getTokenValue()); } - private OidcClientRegistrationAuthenticationToken jwtClientAuthenticationRegistration( - String tokenAuthenticationMethod, String tokenSigningAlgorithm, String jwkSetUrl) { - Jwt jwt = createJwtClientRegistration(); - OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, - jwt.getTokenValue(), jwt.getIssuedAt(), - jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE)); - RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); - OAuth2Authorization authorization = TestOAuth2Authorizations.authorization( - registeredClient, jwtAccessToken, jwt.getClaims()).build(); - when(this.authorizationService.findByToken( - eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN))) - .thenReturn(authorization); - when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwtClientConfiguration()); - - JwtAuthenticationToken principal = new JwtAuthenticationToken( - jwt, AuthorityUtils.createAuthorityList("SCOPE_client.create")); - // @formatter:off - OidcClientRegistration.Builder clientRegistrationBuilder = OidcClientRegistration.builder() - .clientId("client-id") - .clientName("client-name") - .redirectUri("https://client.example.com") - .grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) - .scope("scope1") - .tokenEndpointAuthenticationMethod(tokenAuthenticationMethod); - // @formatter:on - - if (tokenSigningAlgorithm != null) { - clientRegistrationBuilder = clientRegistrationBuilder.tokenEndpointAuthenticationSigningAlgorithm(tokenSigningAlgorithm); - } - - if (jwkSetUrl != null) { - clientRegistrationBuilder = clientRegistrationBuilder.jwkSetUrl(jwkSetUrl); - } - - return new OidcClientRegistrationAuthenticationToken(principal, clientRegistrationBuilder.build()); - } - - @Test - public void authenticateWhenClientRegistrationRequestAndPrivateKeyJwtAndAlgorithmNoneThenThrowOAuth2AuthenticationException() { - OidcClientRegistrationAuthenticationToken authentication = jwtClientAuthenticationRegistration( - ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue(), "none", "https://client.example.com/jwks"); - - assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) - .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode") - .isEqualTo("invalid_client_metadata"); - } - - @Test - public void authenticateWhenClientRegistrationRequestAndPrivateKeyJwtAndMacAlgorithmThenThrowOAuth2AuthenticationException() { - OidcClientRegistrationAuthenticationToken authentication = jwtClientAuthenticationRegistration( - ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue(), "HS256", "https://client.example.com/jwks"); - - assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) - .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode") - .isEqualTo("invalid_client_metadata"); - } - - @Test - public void authenticateWhenClientRegistrationRequestAndPrivateKeyJwtAndNoJwkSetUrlThenThrowOAuth2AuthenticationException() { - OidcClientRegistrationAuthenticationToken authentication = jwtClientAuthenticationRegistration( - ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue(), "RS256", null); - - assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) - .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode") - .isEqualTo("invalid_client_metadata"); - } - - @Test - public void authenticateWhenClientRegistrationRequestAndClientSecretJwtAndPkiAlgorithmThenThrowOAuth2AuthenticationException() { - OidcClientRegistrationAuthenticationToken authentication = jwtClientAuthenticationRegistration( - ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue(), "RS256", "https://client.example.com/jwks"); - - assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) - .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode") - .isEqualTo("invalid_client_metadata"); - } - - @Test - public void authenticateWhenClientRegistrationAndProperClientSecretJwtRegistrationThenRegistered() { - OidcClientRegistrationAuthenticationToken authentication = jwtClientAuthenticationRegistration( - ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue(), "HS512", null); - - Authentication authenticationResult = this.authenticationProvider.authenticate(authentication); - - assertThat(authenticationResult).isNotNull(); - - ArgumentCaptor registeredClientCaptor = ArgumentCaptor.forClass(RegisteredClient.class); - verify(this.registeredClientRepository).save(registeredClientCaptor.capture()); - RegisteredClient registeredClientResult = registeredClientCaptor.getValue(); - - assertThat(registeredClientResult).isNotNull(); - assertThat(registeredClientResult.getClientSecret()).hasSizeGreaterThan(32); - assertThat(registeredClientResult.getClientSettings().getTokenEndpointSigningAlgorithm()).isEqualTo(MacAlgorithm.HS512); - } - - @Test - public void authenticateWhenClientRegistrationAndClientSecretJwtAndNullAlgorithmThenDefaultAlgorithmHS256() { - OidcClientRegistrationAuthenticationToken authentication = jwtClientAuthenticationRegistration( - ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue(), null, null); - - Authentication authenticationResult = this.authenticationProvider.authenticate(authentication); - - assertThat(authenticationResult).isNotNull(); - - ArgumentCaptor registeredClientCaptor = ArgumentCaptor.forClass(RegisteredClient.class); - verify(this.registeredClientRepository).save(registeredClientCaptor.capture()); - RegisteredClient registeredClientResult = registeredClientCaptor.getValue(); - - assertThat(registeredClientResult).isNotNull(); - assertThat(registeredClientResult.getClientSecret()).hasSizeGreaterThan(32); - assertThat(registeredClientResult.getClientSettings().getTokenEndpointSigningAlgorithm()).isEqualTo(MacAlgorithm.HS256); - assertThat(registeredClientResult.getClientAuthenticationMethods()).contains(ClientAuthenticationMethod.CLIENT_SECRET_JWT); - } - - @Test - public void authenticateWhenClientRegistrationAndProperPrivateKeyJwtRegistrationThenRegistered() { - OidcClientRegistrationAuthenticationToken authentication = jwtClientAuthenticationRegistration( - ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue(), "RS512", "https://client.example.com/jwks"); - - OidcClientRegistrationAuthenticationToken authenticationResult = - (OidcClientRegistrationAuthenticationToken) this.authenticationProvider.authenticate(authentication); - - assertThat(authenticationResult).isNotNull(); - - ArgumentCaptor registeredClientCaptor = ArgumentCaptor.forClass(RegisteredClient.class); - verify(this.registeredClientRepository).save(registeredClientCaptor.capture()); - RegisteredClient registeredClientResult = registeredClientCaptor.getValue(); - - assertThat(registeredClientResult).isNotNull(); - assertThat(registeredClientResult.getClientSettings().getJwkSetUrl()).isEqualTo("https://client.example.com/jwks"); - assertThat(registeredClientResult.getClientSettings().getTokenEndpointSigningAlgorithm()).isEqualTo(SignatureAlgorithm.RS512); - - assertThat(authenticationResult.getClientRegistration().getTokenEndpointAuthenticationSigningAlgorithm()).isEqualTo("RS512"); - assertThat(authenticationResult.getClientRegistration().getJwkSetUrl().toString()).isEqualTo("https://client.example.com/jwks"); - assertThat(registeredClientResult.getClientAuthenticationMethods()).contains(ClientAuthenticationMethod.PRIVATE_KEY_JWT); - } - - @Test - public void authenticateWhenClientRegistrationAndPrivateKeyJwtAndNullAlgorithmThenDefaultAlgorithmRS256() { - OidcClientRegistrationAuthenticationToken authentication = jwtClientAuthenticationRegistration( - ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue(), null, "https://client.example.com/jwks"); - - Authentication authenticationResult = this.authenticationProvider.authenticate(authentication); - - assertThat(authenticationResult).isNotNull(); - - ArgumentCaptor registeredClientCaptor = ArgumentCaptor.forClass(RegisteredClient.class); - verify(this.registeredClientRepository).save(registeredClientCaptor.capture()); - RegisteredClient registeredClientResult = registeredClientCaptor.getValue(); - - assertThat(registeredClientResult).isNotNull(); - assertThat(registeredClientResult.getClientSettings().getJwkSetUrl()).isEqualTo("https://client.example.com/jwks"); - assertThat(registeredClientResult.getClientSettings().getTokenEndpointSigningAlgorithm()).isEqualTo(SignatureAlgorithm.RS256); - } - @Test public void authenticateWhenClientConfigurationRequestAndAccessTokenNotAuthorizedThenThrowOAuth2AuthenticationException() { Jwt jwt = createJwt(Collections.singleton("unauthorized.scope")); @@ -707,7 +680,16 @@ public class OidcClientRegistrationAuthenticationProviderTests { jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE)); RegisteredClient registeredClient = TestRegisteredClients.registeredClient() - .clientName("client-name") + .clientAuthenticationMethods((clientAuthenticationMethods) -> { + clientAuthenticationMethods.clear(); + clientAuthenticationMethods.add(ClientAuthenticationMethod.PRIVATE_KEY_JWT); + }) + .clientSettings( + ClientSettings.builder() + .tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS512) + .jwkSetUrl("https://client.example.com/jwks") + .build() + ) .build(); OAuth2Authorization authorization = TestOAuth2Authorizations.authorization( registeredClient, jwtAccessToken, jwt.getClaims()).build(); @@ -755,6 +737,10 @@ public class OidcClientRegistrationAuthenticationProviderTests { .containsExactlyInAnyOrderElementsOf(registeredClient.getScopes()); assertThat(clientRegistrationResult.getTokenEndpointAuthenticationMethod()) .isEqualTo(registeredClient.getClientAuthenticationMethods().iterator().next().getValue()); + assertThat(clientRegistrationResult.getTokenEndpointAuthenticationSigningAlgorithm()) + .isEqualTo(registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm().getName()); + assertThat(clientRegistrationResult.getJwkSetUrl().toString()) + .isEqualTo(registeredClient.getClientSettings().getJwkSetUrl()); assertThat(clientRegistrationResult.getIdTokenSignedResponseAlgorithm()) .isEqualTo(registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm().getName()); 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 3dbd4f89..6a51d4d1 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 @@ -15,7 +15,6 @@ */ package org.springframework.security.oauth2.server.authorization.web; - import javax.servlet.FilterChain; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/authentication/JwtClientAssertionAuthenticationConverterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/authentication/JwtClientAssertionAuthenticationConverterTests.java index a2da3ad0..9ffc08fe 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/authentication/JwtClientAssertionAuthenticationConverterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/authentication/JwtClientAssertionAuthenticationConverterTests.java @@ -13,45 +13,35 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.security.oauth2.server.authorization.web.authentication; import org.junit.Test; + import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; /** - * Tests for {@link JwtClientAssertionAuthenticationConverter} + * Tests for {@link JwtClientAssertionAuthenticationConverter}. * * @author Rafal Lewczuk */ public class JwtClientAssertionAuthenticationConverterTests { - - private JwtClientAssertionAuthenticationConverter converter = new JwtClientAssertionAuthenticationConverter(); - private static final String JWT_BEARER_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; - - private void shouldThrow(MockHttpServletRequest request, String errorCode) { - assertThatThrownBy(() -> this.converter.convert(request)) - .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) - .extracting("errorCode") - .isEqualTo(errorCode); - } + private final JwtClientAssertionAuthenticationConverter converter = new JwtClientAssertionAuthenticationConverter(); @Test - public void convertWhenClientAssertionTypeNullThenReturnNull() { + public void convertWhenMissingClientAssertionTypeThenReturnNull() { MockHttpServletRequest request = new MockHttpServletRequest(); - request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "some_jwt_assertion"); + request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "jwt-assertion"); Authentication authentication = this.converter.convert(request); assertThat(authentication).isNull(); } @@ -65,84 +55,74 @@ public class JwtClientAssertionAuthenticationConverterTests { } @Test - public void convertWhenMissingClientIdThenInvalidRequestError() { + public void convertWhenMultipleClientAssertionTypeThenInvalidRequestError() { MockHttpServletRequest request = new MockHttpServletRequest(); request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_BEARER_TYPE); - request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "some_jwt_assertion"); - shouldThrow(request, OAuth2ErrorCodes.INVALID_REQUEST); + request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, "other-client-assertion-type"); + request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "jwt-assertion"); + assertThrown(request, OAuth2ErrorCodes.INVALID_REQUEST); } @Test - public void convertWhenMultipleClientIdThenInvalidRequestError() { + public void convertWhenNotJwtAssertionTypeThenReturnNull() { MockHttpServletRequest request = new MockHttpServletRequest(); - request.addParameter(OAuth2ParameterNames.CLIENT_ID, "some_client"); - request.addParameter(OAuth2ParameterNames.CLIENT_ID, "other_client"); - request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_BEARER_TYPE); - request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "some_jwt_assertion"); - shouldThrow(request, OAuth2ErrorCodes.INVALID_REQUEST); - } - - @Test - public void convertWhenBadAssertionTypeThenInvalidRequestError() { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.addParameter(OAuth2ParameterNames.CLIENT_ID, "some_client"); - request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, "borken"); - request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "some_jwt_assertion"); - shouldThrow(request, OAuth2ErrorCodes.INVALID_REQUEST); - } - - @Test - public void convertWhenMissingClientJwtAssertionTypeThenDoNotProcessClientIdAndReturnNull() { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.addParameter(OAuth2ParameterNames.CLIENT_ID, "some_client"); - request.addParameter(OAuth2ParameterNames.CLIENT_ID, "throw_something_when_client_id_is_processed"); + request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, "other-client-assertion-type"); + request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "other-assertion"); Authentication authentication = this.converter.convert(request); assertThat(authentication).isNull(); } @Test - public void convertWhenMultipleAssertionsThenInvalidRequestError() { + public void convertWhenMultipleClientAssertionThenInvalidRequestError() { MockHttpServletRequest request = new MockHttpServletRequest(); - request.addParameter(OAuth2ParameterNames.CLIENT_ID, "some_client"); request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_BEARER_TYPE); - request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "some_jwt_assertion"); - request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "other_jwt_assertion"); - shouldThrow(request, OAuth2ErrorCodes.INVALID_REQUEST); + request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "jwt-assertion"); + request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "other-jwt-assertion"); + assertThrown(request, OAuth2ErrorCodes.INVALID_REQUEST); } @Test - public void convertWhenValidAssertionJwt() { + public void convertWhenMissingClientIdThenInvalidRequestError() { MockHttpServletRequest request = new MockHttpServletRequest(); - request.addParameter(OAuth2ParameterNames.CLIENT_ID, "some_client"); request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_BEARER_TYPE); - request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "some_jwt_assertion"); - request.setRequestURI("/oauth2/token"); - OAuth2ClientAuthenticationToken authentication = (OAuth2ClientAuthenticationToken) this.converter.convert(request); - assertThat(authentication).isNotNull(); - assertThat(authentication.getRequestUri()).isEqualTo("/oauth2/token"); - assertThat(authentication.getPrincipal()).isEqualTo("some_client"); - assertThat(authentication.getCredentials()).isEqualTo("some_jwt_assertion"); + request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "jwt-assertion"); + assertThrown(request, OAuth2ErrorCodes.INVALID_REQUEST); } @Test - public void convertWhenConfidentialClientWithPkceParametersThenAdditionalParametersIncluded() { + public void convertWhenMultipleClientIdThenInvalidRequestError() { MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_BEARER_TYPE); + request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "jwt-assertion"); + request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-1"); + request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-2"); + assertThrown(request, OAuth2ErrorCodes.INVALID_REQUEST); + } + + @Test + public void convertWhenJwtAssertionThenReturnClientAuthenticationToken() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_BEARER_TYPE); + request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "jwt-assertion"); + request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-1"); request.addParameter(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); request.addParameter(OAuth2ParameterNames.CODE, "code"); - request.addParameter(PkceParameterNames.CODE_VERIFIER, "code-verifier-1"); - request.addParameter(OAuth2ParameterNames.CLIENT_ID, "some_client"); - request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_BEARER_TYPE); - request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "some_jwt_assertion"); - request.setRequestURI("/oauth2/token"); OAuth2ClientAuthenticationToken authentication = (OAuth2ClientAuthenticationToken) this.converter.convert(request); - assertThat(authentication).isNotNull(); - assertThat(authentication.getRequestUri()).isEqualTo("/oauth2/token"); - assertThat(authentication.getPrincipal()).isEqualTo("some_client"); - assertThat(authentication.getCredentials()).isEqualTo("some_jwt_assertion"); + assertThat(authentication.getPrincipal()).isEqualTo("client-1"); + assertThat(authentication.getCredentials()).isEqualTo("jwt-assertion"); + assertThat(authentication.getClientAuthenticationMethod().getValue()).isEqualTo(JWT_BEARER_TYPE); assertThat(authentication.getAdditionalParameters()) .containsOnly( entry(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()), - entry(OAuth2ParameterNames.CODE, "code"), - entry(PkceParameterNames.CODE_VERIFIER, "code-verifier-1")); + entry(OAuth2ParameterNames.CODE, "code")); } + + private void assertThrown(MockHttpServletRequest request, String errorCode) { + assertThatThrownBy(() -> this.converter.convert(request)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .extracting("errorCode") + .isEqualTo(errorCode); + } + }