DEV Community

Greg Simons
Greg Simons

Posted on

Spring Security private_key_jwt with AWS KMS

Spring security has long had great OAuth2.0 support from both the server and client elements. Recently spring security added support for the private_key_jwt client authentication method as part of the authorization code grant flow.
Spring Security GitHub ref

The configuration for the key signing requires both the public and private key to be available to the application. Key Management Service (KMS) by Amazon Web Services (AWS) provides services to centrally manage your keys for encryption and signing and is an option to allow a more centralised mechanism for key rotation and policy management.

In order to add this support in to Spring it requires a number of customisations to be made which will be explained in this post. If your AuthZ server also supports elliptic-curve digital signature algorithms (ECDSA) for the ID token I will outline the additional Bean configuration required as Spring security defaults to RS256 supported with RSA keys.

private_key_jwt

The private key jwt client authentication method requires a jwt token to be sent alongside client id, code as a client assertion to the token endpoint. This jwt will need to be signed and then sent in a client_assertion parameter to the auth server. A number of algorithms are supported by various auth servers and can be found by visiting the configuration endpoint of the auth server.

Full details of the spec can be found here rfc 7523

The first configuration change required is to switch out the default access token response client as part of the HttpSecurity config. This provides the ability to customise the token endpoint request parameters to enrich with the client assertion signed by kms.

@Bean
public Security FilterChain filterchain(HttpSecurity http) throws Exception {
  ....
  http.oauth2Login(oauth2Login ->
    oauthLogin.tokenEndpoint(tokenEndpoint ->
      tokenEndpoint.accessTokenResponseClient(accessTokenResponseClient))).
    .oauth2Client.authorizationCodeGrant(authorizationCodeGrant ->
    authorizationCodeGrant.accessTokenResponseClient(accessTokenResponseClient)));

  ....
  http.build();
}
Enter fullscreen mode Exit fullscreen mode

OAuth2AccessTokenResponseClient

In order to override the request entity handling to add support for the kms signing you need to add a custom request entity converter to the access token response client that you create.

...
@Bean
public OAuth2AccessTokenResponseClient<OAuthAuthorizationCodeGrantRequest accessTokenResponseClient() {
  ....
  DefaultAuthorizationCodeTokenResponseClient client = new DefaultAuthorizationCodeTokenResponseClient();
  client.setRequestEntityConverter(converter);
  return client;
}
Enter fullscreen mode Exit fullscreen mode

From this point it is now necessary to create the converter:

It is important to note whether or not you are creating the beans or are they being managed and created by Spring as you could incur a number of errors if the objects are manually instantiated.

The converter is the main class that allows the overriding of the request parameters.

@Component
public class CustomKMSJWTClientAuthNConverter implements Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> {

  private OAuth2AuthorizationCodeGrantRequestEntityConverter defaultConverter;

  public CustomKMSJWTClientAuthNConverter () {
    defaultConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
  }

  public RequestEntity<?> convert(OAuth2AuthorizationCodeGrantRequest req) {
    String signedJWT = createSignedJwt();
    RequestEntity<?> entity = defaultConverter.convert(req);
    MultiValueMap<String, String> parameters = (MultiValueMap<String, String>) entity.getBody();
    parameters.set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
    parameters.set("client_assertion", signedJWT);
    return new RequestEntity<>(parameters, entity.getHeaders(), entity.getMethod(), entity.getUrl());
  }
}

Enter fullscreen mode Exit fullscreen mode

Creating a signed JWT

The next step is to create the JWT and use the KMS API to sign the token.

You will need to setup the AWS SDK and KMS Client. For example, I exported the AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN and AWS_ACCESS_KEY_ID environment variables and set them on the command line. There are a number of alternatives supported in spring that can be found here: AWS SDK on Spring

  kmsClient = KmsClient.builder().region(Region.of("....."))
      .credentialsProvider(DefaultCredentialsProvider.create()
      .build();
Enter fullscreen mode Exit fullscreen mode

  // tbc values can be configured with your auth server

  public String createSignedJwt() {
    JWTClaimSet.Builder claimSetBuilder = new JWTClaimSet.Builder().audience('tbc').issuer('tbc').subject('tbc').expirationTime(tbc).issueTime(Date.from(Instant.now())).jwtId('tbc');

    JWTClaimSet claimSet = claimSetBuilder.build();
    return sign(claimSet);
  }

Enter fullscreen mode Exit fullscreen mode

We now have a JWT token ready to sign and verify with KMS.

  public String sign(JWTClaimSet claimSet) {
      ....
      // choose a token alg based on what is supported by your auth Server
      JWSHeader header = new JWSHeader(TOKEN_ALG);

      Base64URL encodedHeader = header.toBase64URL();
      Base64URL encodedClaims = Base64URL.encode(claimSet.toString());
      // create String to sign with KMS
      String signingString = encodedHeader + "." + encodedClaims;
      ....
    }
  }

Enter fullscreen mode Exit fullscreen mode

We now need to use the AWS SDK to create the signing request to pass to KMS to get the signed JWT.

Key Id is the id of your key from AWS.
Signing alg can be selected from a predefined set using
software.amazon.awssdk.services.kms.model.SigningAlgorithmSpec;

  ....
  SignRequest signRequest = SignRequest.builder()

  .message(SDKBytes.fromByteArray(signingString.getBytes()))
  .keyId(KEY_ID)
  .signingAlgorithm(SIGNING_ALG)
  .build();

  SignResponse response = kmsClient.sign(signRequest);
  String signature = Base64.encode(response.signature().asByteArray()).toString();
  return signature;

Enter fullscreen mode Exit fullscreen mode

The above call now provides a Base64 encoded version of the signed string that is attached to the request parameters for the invocation of the token endpoint.

ID Token Verification

Depending on the supported algorithms for the id token you might find that the ID Token is signed with a different algorithm than the default RS256 that spring supports. In order to override this setting you can again create a custom Bean that sets alternative signatures using the JWSAlgorithmResolver.

@Bean
public JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
  OidcTokenDecoderFactory idTokenDecoderFactory = new OidcTokenDecoderFactory();
  idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> SignatureAlgorithm.RS512);
return idTokenDecoderFactory;
}
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
manojparshi profile image
Manoj Kumar Parshi • Edited

Hi Greg

if possible could you share github repo link for the same.

Thanks