diff --git a/config/src/main/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParser.java index 62b28ddb5fd..9c2b65a4bf2 100644 --- a/config/src/main/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParser.java @@ -50,6 +50,11 @@ public final class ClientRegistrationsBeanDefinitionParser implements BeanDefini private static final String ATT_CLIENT_ID = "client-id"; private static final String ATT_CLIENT_SECRET = "client-secret"; private static final String ATT_CLIENT_AUTHENTICATION_METHOD = "client-authentication-method"; + private static final String ATT_CLIENT_AUTHENTICATION_KEY_STORE = "client-authentication-key-store"; + private static final String ATT_CLIENT_AUTHENTICATION_KEY_STORE_TYPE = "client-authentication-key-store-type"; + private static final String ATT_CLIENT_AUTHENTICATION_KEY_STORE_PASSWORD = "client-authentication-key-store-password"; + private static final String ATT_CLIENT_AUTHENTICATION_KEY_ALIAS = "client-authentication-key-alias"; + private static final String ATT_CLIENT_AUTHENTICATION_KEY_PASSWORD = "client-authentication-key-password"; private static final String ATT_AUTHORIZATION_GRANT_TYPE = "authorization-grant-type"; private static final String ATT_REDIRECT_URI = "redirect-uri"; private static final String ATT_SCOPE = "scope"; @@ -110,6 +115,16 @@ private List getClientRegistrations(Element element, ParserC getOptionalIfNotEmpty(clientRegistrationElt.getAttribute(ATT_CLIENT_AUTHENTICATION_METHOD)) .map(ClientAuthenticationMethod::new) .ifPresent(builder::clientAuthenticationMethod); + getOptionalIfNotEmpty(clientRegistrationElt.getAttribute(ATT_CLIENT_AUTHENTICATION_KEY_STORE)) + .map(builder::clientAuthenticationKeyStore); + getOptionalIfNotEmpty(clientRegistrationElt.getAttribute(ATT_CLIENT_AUTHENTICATION_KEY_STORE_TYPE)) + .map(builder::clientAuthenticationKeyStoreType); + getOptionalIfNotEmpty(clientRegistrationElt.getAttribute(ATT_CLIENT_AUTHENTICATION_KEY_STORE_PASSWORD)) + .map(builder::clientAuthenticationKeyStorePassword); + getOptionalIfNotEmpty(clientRegistrationElt.getAttribute(ATT_CLIENT_AUTHENTICATION_KEY_ALIAS)) + .map(builder::clientAuthenticationKeyAlias); + getOptionalIfNotEmpty(clientRegistrationElt.getAttribute(ATT_CLIENT_AUTHENTICATION_KEY_PASSWORD)) + .map(builder::clientAuthenticationKeyPassword); getOptionalIfNotEmpty(clientRegistrationElt.getAttribute(ATT_AUTHORIZATION_GRANT_TYPE)) .map(AuthorizationGrantType::new) .ifPresent(builder::authorizationGrantType); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java index 2991b855a8d..9656063e6bd 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java @@ -15,6 +15,15 @@ */ package org.springframework.security.oauth2.client.endpoint; +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.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -29,8 +38,8 @@ import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; -import java.util.Collections; -import java.util.Set; +import java.security.KeyPair; +import java.util.*; import static org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors.oauth2AccessTokenResponse; @@ -54,6 +63,11 @@ abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient implements ReactiveOAuth2AccessTokenResponseClient { + private static final Log logger = LogFactory + .getLog(AbstractWebClientReactiveOAuth2AccessTokenResponseClient.class); + + public static String CLIENT_ASSERTION_TYPE_JWT_BEARER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; + private WebClient webClient = WebClient.builder().build(); @Override @@ -106,6 +120,89 @@ private BodyInserters.FormInserter createTokenRequestBody(T grantRequest return populateTokenRequestBody(grantRequest, body); } + /** + *

Creates a {@link JWTClaimsSet} to be used + * when signing for client authentication methods {@link ClientAuthenticationMethod#JWT} or + * {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT}. + * + * @param clientRegistration for claim information + * @return the claims to be signed + */ + private JWTClaimsSet jwtClaimsSet(ClientRegistration clientRegistration) { + + // TODO: should we include `iat` even though it's optional? + // iss - [REQUIRED] Issuer. This must contain the client_id of the OAuth Client. + // sub - [REQUIRED] Subject. This must contain the client_id of the OAuth Client. + // aud - [REQUIRED] Audience. The aud (audience) Claim. A value that identifies the Authorization Server as an intended audience. The Authorization Server must verify that it is an intended audience for the token. The Audience should be the URL of the Authorization Server's Token Endpoint. + // jti - [REQUIRED] JWT ID. A unique identifier for the token, which can be used to prevent reuse of the token. These tokens must only be used once unless conditions for reuse were negotiated between the parties; any such negotiation is beyond the scope of this specification. + // exp - [REQUIRED] Expiration time on or after which the JWT must not be accepted for processing. + // iat - [OPTIONAL] Time at which the JWT was issued. + + String clientId = clientRegistration.getClientId(); + String tokenUri = clientRegistration.getProviderDetails().getTokenUri(); + String jwtId = UUID.randomUUID().toString(); + + // TODO: make this default to 5 minutes, unless configured otherwise; + long expiresIn = 300000L; // 5 minutes + Date expiresAt = new Date(System.currentTimeMillis() + expiresIn); + + List aud = new ArrayList<>(); + aud.add(tokenUri); + return new JWTClaimsSet.Builder() + .issuer(clientId) + .subject(clientId) + .audience(aud) + .jwtID(jwtId) + .expirationTime(expiresAt) + .build(); + } + + /** + *

Creates a signed JWT to be used + * when signing for client authentication methods {@link ClientAuthenticationMethod#JWT}. + * + * @param clientRegistration for client secret and claim information + * @return the signed client secret JWT + */ + private String signClientSecretJwt(ClientRegistration clientRegistration) { + try { + String clientSecret = clientRegistration.getClientSecret(); + JWTClaimsSet claimsSet = jwtClaimsSet(clientRegistration); + JWSSigner signer = new MACSigner(clientSecret); + SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSet); + signedJWT.sign(signer); + return signedJWT.serialize(); + } catch (Exception e) { + // an empty JWT will cause an error downstream, so we will log an error, but continue the flow + logger.error("Failed to sign client secret JWT.", e); + } + return ""; + } + + /** + *

Creates a signed JWT to be used + * when signing for client authentication methods {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT}. + * + * @param clientRegistration for private key and claim information + * @return the signed private key JWT + */ + private String signPrivateKeyJwt(ClientRegistration clientRegistration) { + try { + if (clientRegistration.getClientAuthenticationKeyPair() != null) { + KeyPair keyPair = clientRegistration.getClientAuthenticationKeyPair(); + JWTClaimsSet claimsSet = jwtClaimsSet(clientRegistration); + JWSSigner signer = new RSASSASigner(keyPair.getPrivate()); + SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSet); + signedJWT.sign(signer); + return signedJWT.serialize(); + } + } catch (Exception e) { + // an empty JWT will cause an error downstream, so we will log an error, but continue the flow + logger.error("Failed to sign private key JWT.", e); + } + return ""; + } + /** * Populates the body of the token request. * @@ -124,6 +221,41 @@ BodyInserters.FormInserter populateTokenRequestBody(T grantRequest, Body if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) { body.with(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret()); } + if (ClientAuthenticationMethod.JWT.equals(clientRegistration.getClientAuthenticationMethod())) { + + // TODO: mention thes comments in documentation? + // ** Client Secret JWT ** + // The JWT must be signed using an HMAC SHA algorithm, + // such as HMAC SHA-256. The HMAC (Hash-based Message Authentication Code) + // is calculated using the octets of the UTF-8 representation of the client-secret as the shared key. + + body.with(OAuth2ParameterNames.CLIENT_ASSERTION, signClientSecretJwt(clientRegistration)); + body.with(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, CLIENT_ASSERTION_TYPE_JWT_BEARER); + } + if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.equals(clientRegistration.getClientAuthenticationMethod())) { + + // TODO: mention thes comments in documentation? + // ** Private Key JWT ** + // The JWT must be signed using an HMAC SHA algorithm, such as SHA-256. + // A public key used for signature verification must be registered at the authorization server. + + // NOTES: + // The main benefit of this method is you can generate the private key on your own servers and never have + // it leave there for any reason + + // creating a JKS + // ./keytool -genkeypair -keyalg RSA \ + // -keystore ${KEY_STORE} \ + // -storepass ${KEY_STORE_PASSWORD} \ + // -alias ${KEY_ALIAS} \ + // -keypass ${KEY_PASS} + + // extracting the public key from the JKS + // keytool -list -rfc --keystore ${KEY_STORE} | openssl x509 -inform pem -pubkey + + body.with(OAuth2ParameterNames.CLIENT_ASSERTION, signPrivateKeyJwt(clientRegistration)); + body.with(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, CLIENT_ASSERTION_TYPE_JWT_BEARER); + } Set scopes = scopes(grantRequest); if (!CollectionUtils.isEmpty(scopes)) { body.with(OAuth2ParameterNames.SCOPE, diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java index e2185714562..717fb8df8ac 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java @@ -15,17 +15,6 @@ */ package org.springframework.security.oauth2.client.registration; -import java.io.Serializable; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; - import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; @@ -33,6 +22,12 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import java.io.FileInputStream; +import java.io.Serializable; +import java.security.*; +import java.security.cert.Certificate; +import java.util.*; + import static java.util.Collections.EMPTY_MAP; /** @@ -48,6 +43,12 @@ public final class ClientRegistration implements Serializable { private String clientId; private String clientSecret; private ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.BASIC; + private String clientAuthenticationKeyStore; + private String clientAuthenticationKeyStoreType; + private String clientAuthenticationKeyStorePassword; + private String clientAuthenticationKeyAlias; + private String clientAuthenticationKeyPassword; + private KeyPair clientAuthenticationKeyPair; private AuthorizationGrantType authorizationGrantType; private String redirectUriTemplate; private Set scopes = Collections.emptySet(); @@ -94,6 +95,64 @@ public ClientAuthenticationMethod getClientAuthenticationMethod() { return this.clientAuthenticationMethod; } + /** + * Returns the client authentication key store path used + * when authenticating with method {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT}. + * + * @return the client authentication key store path + */ + public String getClientAuthenticationKeyStore() { + return this.clientAuthenticationKeyStore; + } + + /** + * Returns the client authentication key store type used + * when authenticating with method {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT}. + * + * @return the client authentication key store type + */ + public String getClientAuthenticationKeyStoreType() { + return this.clientAuthenticationKeyStoreType; + } + + /** + * Returns the client authentication key store password used + * when authenticating with method {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT}. + * + * @return the client authentication key store password + */ + public String getClientAuthenticationKeyStorePassword() { + return this.clientAuthenticationKeyStorePassword; + } + + /** + * Returns the client authentication key store key alias used + * when authenticating with method {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT}. + * + * @return the client authentication key store key alias + */ + public String getClientAuthenticationKeyAlias() { + return this.clientAuthenticationKeyAlias; + } + + /** + * Returns the client authentication key store key password used + * when authenticating with method {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT}. + * + * @return the client authentication key store key password + */ + public String getClientAuthenticationKeyPassword() { + return this.clientAuthenticationKeyPassword; + } + + /** + * Returns the client authentication key pair used + * when authenticating with method {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT}. + * + * @return the client authentication key pair + */ + public KeyPair getClientAuthenticationKeyPair() { return this.clientAuthenticationKeyPair; } + /** * Returns the {@link AuthorizationGrantType authorization grant type} used for the client. * @@ -146,6 +205,11 @@ public String toString() { + ", clientId='" + this.clientId + '\'' + ", clientSecret='" + this.clientSecret + '\'' + ", clientAuthenticationMethod=" + this.clientAuthenticationMethod + + ", clientAuthenticationKeyStore=" + this.clientAuthenticationKeyStore + + ", clientAuthenticationKeyStoreType=" + this.clientAuthenticationKeyStoreType + + ", clientAuthenticationKeyStorePassword=" + this.clientAuthenticationKeyStorePassword + + ", clientAuthenticationKeyAlias=" + this.clientAuthenticationKeyAlias + + ", clientAuthenticationKeyPassword=" + this.clientAuthenticationKeyPassword + ", authorizationGrantType=" + this.authorizationGrantType + ", redirectUriTemplate='" + this.redirectUriTemplate + '\'' + ", scopes=" + this.scopes @@ -287,6 +351,12 @@ public static class Builder implements Serializable { private String clientId; private String clientSecret; private ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.BASIC; + private String clientAuthenticationKeyStore; + private String clientAuthenticationKeyStoreType; + private String clientAuthenticationKeyStorePassword; + private String clientAuthenticationKeyAlias; + private String clientAuthenticationKeyPassword; + private KeyPair clientAuthenticationKeyPair; private AuthorizationGrantType authorizationGrantType; private String redirectUriTemplate; private Set scopes; @@ -308,6 +378,12 @@ private Builder(ClientRegistration clientRegistration) { this.clientId = clientRegistration.clientId; this.clientSecret = clientRegistration.clientSecret; this.clientAuthenticationMethod = clientRegistration.clientAuthenticationMethod; + this.clientAuthenticationKeyStore = clientRegistration.clientAuthenticationKeyStore; + this.clientAuthenticationKeyStoreType = clientRegistration.clientAuthenticationKeyStoreType; + this.clientAuthenticationKeyStorePassword = clientRegistration.clientAuthenticationKeyStorePassword; + this.clientAuthenticationKeyAlias = clientRegistration.clientAuthenticationKeyAlias; + this.clientAuthenticationKeyPassword = clientRegistration.clientAuthenticationKeyPassword; + this.clientAuthenticationKeyPair = clientRegistration.clientAuthenticationKeyPair; this.authorizationGrantType = clientRegistration.authorizationGrantType; this.redirectUriTemplate = clientRegistration.redirectUriTemplate; this.scopes = clientRegistration.scopes == null ? null : new HashSet<>(clientRegistration.scopes); @@ -369,6 +445,66 @@ public Builder clientAuthenticationMethod(ClientAuthenticationMethod clientAuthe return this; } + /** + * Sets the client authentication key store path used + * when authenticating with method {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT}. + * + * @return the client authentication key store path + */ + public Builder clientAuthenticationKeyStore(String clientAuthenticationKeyStore) { + this.clientAuthenticationKeyStore = clientAuthenticationKeyStore; + return this; + } + + /** + * Sets the client authentication key store type used + * when authenticating with method {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT}. + * + * @return the client authentication key store type + */ + public Builder clientAuthenticationKeyStoreType(String clientAuthenticationKeyStoreType) { + this.clientAuthenticationKeyStore = clientAuthenticationKeyStoreType; + return this; + } + + /** + * Sets the client authentication key store password used + * when authenticating with method {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT}. + * + * @return the client authentication key store password + */ + public Builder clientAuthenticationKeyStorePassword(String clientAuthenticationKeyStorePassword) { + this.clientAuthenticationKeyStorePassword = clientAuthenticationKeyStorePassword; + return this; + } + + /** + * Sets the client authentication key store key alias used + * when authenticating with method {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT}. + * + * @return the client authentication key store key alias + */ + public Builder clientAuthenticationKeyAlias(String clientAuthenticationKeyAlias) { + this.clientAuthenticationKeyAlias = clientAuthenticationKeyAlias; + return this; + } + + /** + * Sets the client authentication key store key password used + * when authenticating with method {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT}. + * + * @return the client authentication key store key password + */ + public Builder clientAuthenticationKeyPassword(String clientAuthenticationKeyPassword) { + this.clientAuthenticationKeyPassword = clientAuthenticationKeyPassword; + return this; + } + + public Builder clientAuthenticationKeyPair(KeyPair clientAuthenticationKeyPair) { + this.clientAuthenticationKeyPair = clientAuthenticationKeyPair; + return this; + } + /** * Sets the {@link AuthorizationGrantType authorization grant type} used for the client. * @@ -527,6 +663,13 @@ public ClientRegistration build() { } else if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizationGrantType)) { this.validateAuthorizationCodeGrantType(); } + + if (ClientAuthenticationMethod.JWT.equals(this.clientAuthenticationMethod)) { + this.validateClientSecretJwtAuthenticationMethod(); + } else if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.equals(this.clientAuthenticationMethod)) { + this.validatePrivateKeyJwtAuthenticationMethod(); + } + this.validateScopes(); return this.create(); } @@ -539,10 +682,29 @@ private ClientRegistration create() { clientRegistration.clientSecret = StringUtils.hasText(this.clientSecret) ? this.clientSecret : ""; clientRegistration.clientAuthenticationMethod = this.clientAuthenticationMethod; if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizationGrantType) && + !ClientAuthenticationMethod.PRIVATE_KEY_JWT.equals(this.clientAuthenticationMethod) && !StringUtils.hasText(this.clientSecret)) { clientRegistration.clientAuthenticationMethod = ClientAuthenticationMethod.NONE; } + clientRegistration.clientAuthenticationKeyStore = this.clientAuthenticationKeyStore; + clientRegistration.clientAuthenticationKeyStoreType = this.clientAuthenticationKeyStoreType; + clientRegistration.clientAuthenticationKeyStorePassword = this.clientAuthenticationKeyStorePassword; + clientRegistration.clientAuthenticationKeyAlias = this.clientAuthenticationKeyAlias; + clientRegistration.clientAuthenticationKeyPassword = this.clientAuthenticationKeyPassword; + + if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.equals(this.clientAuthenticationMethod) && + this.clientAuthenticationKeyPair == null + ) { + clientRegistration.clientAuthenticationKeyPair = keyPair( + clientRegistration.clientAuthenticationKeyStore, + clientRegistration.clientAuthenticationKeyStorePassword, + clientRegistration.clientAuthenticationKeyAlias, + clientRegistration.clientAuthenticationKeyPassword, + clientRegistration.clientAuthenticationKeyStoreType + ); + } + clientRegistration.authorizationGrantType = this.authorizationGrantType; clientRegistration.redirectUriTemplate = this.redirectUriTemplate; clientRegistration.scopes = this.scopes; @@ -598,6 +760,23 @@ private void validatePasswordGrantType() { Assert.hasText(this.tokenUri, "tokenUri cannot be empty"); } + private void validateClientSecretJwtAuthenticationMethod() { + Assert.isTrue(ClientAuthenticationMethod.JWT.equals(this.clientAuthenticationMethod), + () -> "clientAuthenticationMethod must be " + ClientAuthenticationMethod.JWT.getValue()); + Assert.hasText(this.clientSecret, "clientSecret cannot be empty"); + } + + private void validatePrivateKeyJwtAuthenticationMethod() { + Assert.isTrue(ClientAuthenticationMethod.PRIVATE_KEY_JWT.equals(this.clientAuthenticationMethod), + () -> "clientAuthenticationMethod must be " + ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()); + if(this.clientAuthenticationKeyPair == null) { + Assert.hasText(this.clientAuthenticationKeyStore, "clientAuthenticationKeyStore cannot be empty"); + Assert.hasText(this.clientAuthenticationKeyAlias, "clientAuthenticationKeyAlias cannot be empty"); + Assert.isTrue((this.clientAuthenticationKeyStorePassword != null) || + (this.clientAuthenticationKeyPassword != null), "clientAuthenticationKeyStorePassword and clientAuthenticationKeyPassword cannot both be null"); + } + } + private void validateScopes() { if (this.scopes == null) { return; @@ -619,5 +798,34 @@ private static boolean validateScope(String scope) { private static boolean withinTheRangeOf(int c, int min, int max) { return c >= min && c <= max; } + + private static KeyPair keyPair(String keyStorePath, String keyStorePassword, String keyAlias, String keyPassword, String keyStoreType) { + KeyPair keyPair = null; + try { + String type = KeyStore.getDefaultType(); + if (keyStoreType != null) { + type = keyStoreType; + } + KeyStore keyStore = KeyStore.getInstance(type); + + FileInputStream keyStoreStream = new FileInputStream(keyStorePath); + keyStore.load(keyStoreStream, keyStorePassword.toCharArray()); + + char[] password = null; + if (keyPassword != null) { + password = keyPassword.toCharArray(); + } + Key key = keyStore.getKey(keyAlias, password); + + if (key instanceof PrivateKey) { + Certificate certificate = keyStore.getCertificate(keyAlias); + PublicKey publicKey = certificate.getPublicKey(); + keyPair = new KeyPair(publicKey, (PrivateKey) key); + } + } catch (Exception e) { + return null; + } + return keyPair; + } } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java index c6cbaebf550..4c8f9f5dc9e 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java @@ -16,20 +16,12 @@ package org.springframework.security.oauth2.client.registration; -import java.net.URI; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Supplier; - import com.nimbusds.oauth2.sdk.GrantType; import com.nimbusds.oauth2.sdk.ParseException; import com.nimbusds.oauth2.sdk.Scope; import com.nimbusds.oauth2.sdk.as.AuthorizationServerMetadata; import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; import net.minidev.json.JSONObject; - import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.RequestEntity; import org.springframework.security.oauth2.core.AuthorizationGrantType; @@ -41,6 +33,13 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; +import java.net.URI; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + /** * Allows creating a {@link ClientRegistration.Builder} from an * OpenID Provider Configuration @@ -260,10 +259,17 @@ private static ClientAuthenticationMethod getClientAuthenticationMethod(String i if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_POST)) { return ClientAuthenticationMethod.POST; } + if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_JWT)) { + return ClientAuthenticationMethod.JWT; + } + if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { + return ClientAuthenticationMethod.PRIVATE_KEY_JWT; + } if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.NONE)) { return ClientAuthenticationMethod.NONE; } - throw new IllegalArgumentException("Only ClientAuthenticationMethod.BASIC, ClientAuthenticationMethod.POST and " + throw new IllegalArgumentException("Only ClientAuthenticationMethod.BASIC, ClientAuthenticationMethod.POST, " + + "ClientAuthenticationMethod.JWT, ClientAuthenticationMethod.PRIVATE_KEY_JWT, and " + "ClientAuthenticationMethod.NONE are supported. The issuer \"" + issuer + "\" returned a configuration of " + metadataAuthMethods); } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java index 48ecaaa0ca2..c2b16d55dc7 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java @@ -31,6 +31,8 @@ public final class ClientAuthenticationMethod implements Serializable { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; public static final ClientAuthenticationMethod BASIC = new ClientAuthenticationMethod("basic"); public static final ClientAuthenticationMethod POST = new ClientAuthenticationMethod("post"); + public static final ClientAuthenticationMethod JWT = new ClientAuthenticationMethod("jwt"); + public static final ClientAuthenticationMethod PRIVATE_KEY_JWT = new ClientAuthenticationMethod("private_key_jwt"); /** * @since 5.2 diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java index 3bb5b2e910e..298b79924fa 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java @@ -95,6 +95,16 @@ public interface OAuth2ParameterNames { */ String PASSWORD = "password"; + /** + * {@code client_assertion} - used in Access Token Request. + */ + String CLIENT_ASSERTION = "client_assertion"; + + /** + * {@code client_assertion} - used in Access Token Request. + */ + String CLIENT_ASSERTION_TYPE = "client_assertion_type"; + /** * {@code error} - used in Authorization Response and Access Token Response. */