带有AWS KMS的Spring Security Private_key_jwt
#java #springboot #oauth2 #springsecurity

Spring Security长期以来已经获得了服务器和客户端元素的大量OAUTH2.0支持。最近,Spring Security增加了对Private_key_jwt客户端身份验证方法的支持,作为授权代码授予流的一部分。
Spring Security GitHub ref

密钥签名的配置需要公共密钥和私钥。亚马逊Web服务(AWS)的密钥管理服务(KMS)提供服务以中心管理您的密钥进行加密和签名,并且是允许更具集中式机制进行密钥旋转和策略管理的选择。

为了将此支持添加到春季,需要进行许多自定义,这将在本文中进行解释。如果您的authz服务器还支持ID令牌的椭圆曲线数字签名算法(ECDSA),我将概述所需的其他BEAN配置,因为Spring Security默认值为RS256 rsa键。

private_key_jwt

私有密钥JWT客户端身份验证方法要求与客户ID一起发送JWT令牌,代码作为对令牌端点的客户端主张。此JWT将需要签名,然后在客户端_ASSERTION参数中发送到Auth Server。各种AUTH服务器支持许多算法,可以通过访问Auth Server的配置端点来找到。

可以在此处找到规格的完整详细信息rfc 7523

所需的第一个配置更改是将默认访问令牌响应客户端切换为HTTPSECURITY配置的一部分。这提供了自定义令牌端点请求参数的能力,以通过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();
}

oauth2AccessTokenResponseClient

为了覆盖请求实体处理以添加对KMS签名的支持,您需要将自定义请求实体转换器添加到您创建的访问令牌响应客户端。

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

从这一点开始,现在有必要创建转换器:

重要的是要注意您是在创建豆还是在春季进行管理和创建,因为如果对象是手动实例化的,则可能会遇到许多错误。

转换器是允许覆盖请求参数的主要类。

@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());
  }
}

创建签名的JWT

下一步是创建JWT并使用KMS API签名。

您将需要设置AWS SDK和KMS客户端。例如,我导出了AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN和AWS_ACCESS_KEY_ID环境变量,并将它们设置在命令行。春季有许多替代方案可以在这里找到:AWS SDK on Spring

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

  // 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);
  }

我们现在有一个JWT令牌准备签名和验证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;
      ....
    }
  }

我们现在需要使用AWS SDK来创建签名请求以传递到kms以获取签名的JWT。

密钥ID是AWS的密钥ID。
可以使用
从预定义的集合中选择签名ALG 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;

上面的呼叫现在提供了签名字符串的base64编码版本,该版本附加到请求参数以调用令牌端点。

ID令牌验证

根据ID令牌的支持算法,您可能会发现ID令牌与Spring支持的默认RS256签名。为了覆盖此设置,您可以再次创建一个自定义的bean,该bean使用jwsalgorithmresolver设置替代签名。

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