Skip to content

"client secret jwt" and "private key jwt" auth methods #8445

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -110,6 +115,16 @@ private List<ClientRegistration> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -54,6 +63,11 @@
abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient<T extends AbstractOAuth2AuthorizationGrantRequest>
implements ReactiveOAuth2AccessTokenResponseClient<T> {

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
Expand Down Expand Up @@ -106,6 +120,89 @@ private BodyInserters.FormInserter<String> createTokenRequestBody(T grantRequest
return populateTokenRequestBody(grantRequest, body);
}

/**
* <p>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<String> aud = new ArrayList<>();
aud.add(tokenUri);
return new JWTClaimsSet.Builder()
.issuer(clientId)
.subject(clientId)
.audience(aud)
.jwtID(jwtId)
.expirationTime(expiresAt)
.build();
}

/**
* <p>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 "";
}

/**
* <p>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.
*
Expand All @@ -124,6 +221,41 @@ BodyInserters.FormInserter<String> 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<String> scopes = scopes(grantRequest);
if (!CollectionUtils.isEmpty(scopes)) {
body.with(OAuth2ParameterNames.SCOPE,
Expand Down
Loading