Polish gh-293
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<Object, ?> INSTANT_CONVERTER = getConverter(INSTANT_TYPE_DESCRIPTOR);
|
||||
private final ClaimTypeConverter claimTypeConverter;
|
||||
|
||||
@@ -137,6 +139,7 @@ public class OidcClientRegistrationHttpMessageConverter extends AbstractHttpMess
|
||||
Converter<Object, ?> stringConverter = getConverter(STRING_TYPE_DESCRIPTOR);
|
||||
Converter<Object, ?> collectionStringConverter = getConverter(
|
||||
TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR));
|
||||
Converter<Object, ?> urlConverter = getConverter(URL_TYPE_DESCRIPTOR);
|
||||
|
||||
Map<String, Converter<Object, ?>> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<RegisteredClient> 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<ClientAuthenticationMethod> 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<String> 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<RegisteredClient> {
|
||||
|
||||
private static final String CLIENT_ASSERTION_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7523#section-3";
|
||||
private static class JwtClientAssertionDecoderFactory implements JwtDecoderFactory<RegisteredClient> {
|
||||
private static final String JWT_CLIENT_AUTHENTICATION_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7523#section-3";
|
||||
|
||||
private static final Map<JwsAlgorithm, String> JCA_ALGORITHM_MAPPINGS;
|
||||
|
||||
static {
|
||||
Map<JwsAlgorithm, String> 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<RegisteredClient, JwsAlgorithm> jwsAlgorithmResolver =
|
||||
rc -> rc.getClientSettings().getTokenEndpointSigningAlgorithm();
|
||||
private final Map<String, JwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
|
||||
private List<String> providerAudience = Collections.emptyList();
|
||||
|
||||
private final Map<String, CachedJwtDecoder> 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<Jwt> createTokenValidator(RegisteredClient registeredClient) {
|
||||
private OAuth2TokenValidator<Jwt> createJwtValidator(RegisteredClient registeredClient) {
|
||||
String clientId = registeredClient.getClientId();
|
||||
return new DelegatingOAuth2TokenValidator<>(
|
||||
new JwtClaimValidator<String>("iss", clientId::equals), // RFC 7523 section 3 (iss)
|
||||
new JwtClaimValidator<String>("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<List<String>> 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<String> getProviderAudience(ProviderSettings providerSettings) {
|
||||
if (!StringUtils.hasText(providerSettings.getIssuer())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String, Object> 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<String, Object> 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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -37,7 +37,7 @@ import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
* <li>{@link HashSetMixin}</li>
|
||||
* <li>{@link OAuth2AuthorizationRequestMixin}</li>
|
||||
* <li>{@link DurationMixin}</li>
|
||||
* <li>{@link SignatureAlgorithmMixin}</li>
|
||||
* <li>{@link JwsAlgorithmMixin}</li>
|
||||
* </ul>
|
||||
*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<List<String>> 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();
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -69,10 +69,12 @@ public final class ClientSecretPostAuthenticationConverter implements Authentica
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
Map<String, Object> additionalParameters = OAuth2EndpointUtils.extractAdditionalParameters(request,
|
||||
OAuth2ParameterNames.CLIENT_ID, OAuth2ParameterNames.CLIENT_SECRET);
|
||||
Map<String, Object> additionalParameters = OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(request,
|
||||
OAuth2ParameterNames.CLIENT_ID,
|
||||
OAuth2ParameterNames.CLIENT_SECRET);
|
||||
|
||||
return new OAuth2ClientAuthenticationToken(clientId, ClientAuthenticationMethod.CLIENT_SECRET_POST, clientSecret,
|
||||
additionalParameters);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String, String> 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<String, String> 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<String, Object> additionalParameters = OAuth2EndpointUtils.extractAdditionalParameters(request,
|
||||
OAuth2ParameterNames.CLIENT_ID,
|
||||
Map<String, Object> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -54,6 +54,17 @@ final class OAuth2EndpointUtils {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
static Map<String, Object> getParametersIfMatchesAuthorizationCodeGrantRequest(HttpServletRequest request, String... exclusions) {
|
||||
if (!matchesAuthorizationCodeGrantRequest(request)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, Object> 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<String, Object> extractAdditionalParameters(HttpServletRequest request, String...exclusions) {
|
||||
Map<String, Object> additionalParameters = Collections.emptyMap();
|
||||
if (OAuth2EndpointUtils.matchesAuthorizationCodeGrantRequest(request)) {
|
||||
additionalParameters = new HashMap<>(OAuth2EndpointUtils.getParameters(request).toSingleValueMap());
|
||||
for (String exclusion : exclusions) {
|
||||
additionalParameters.remove(exclusion);
|
||||
}
|
||||
}
|
||||
return additionalParameters;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
|
||||
new OAuth2AccessTokenResponseHttpMessageConverter();
|
||||
private static final HttpMessageConverter<OidcClientRegistration> clientRegistrationHttpMessageConverter =
|
||||
new OidcClientRegistrationHttpMessageConverter();
|
||||
private static EmbeddedDatabase db;
|
||||
private static JWKSource<SecurityContext> 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<String> 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<String> 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <T>
|
||||
*/
|
||||
public class ValueCaptureMatcher<T> extends BaseMatcher<T> {
|
||||
|
||||
private ClassCastException castException;
|
||||
private List<T> values = new ArrayList<>();
|
||||
|
||||
public T lastValue() {
|
||||
return values.isEmpty() ? null : values.get(values.size()-1);
|
||||
}
|
||||
|
||||
public List<T> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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()
|
||||
|
||||
@@ -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\"");
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<RegisteredClient> 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<SecurityContext> 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<String, Object> createAuthorizationCodeTokenParameters() {
|
||||
Map<String, Object> 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<String, Object> headers = new HashMap<>();
|
||||
headers.put("kid", "123");
|
||||
Map<String, Object> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<RegisteredClient> registeredClientDecoderFactory;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
OAuth2ClientAuthenticationProvider authenticationProvider = new OAuth2ClientAuthenticationProvider(
|
||||
mock(RegisteredClientRepository.class), mock(OAuth2AuthorizationService.class));
|
||||
this.registeredClientDecoderFactory = (JwtDecoderFactory<RegisteredClient>)
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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.<String>getSetting("name1")).isEqualTo("value1");
|
||||
assertThat(clientSettings.<String>getSetting("name2")).isEqualTo("value2");
|
||||
}
|
||||
|
||||
@@ -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<RegisteredClient> 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<RegisteredClient> 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<RegisteredClient> 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<RegisteredClient> 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());
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user