Search code examples
spring-securityopenid-connectspring-security-oauth2

"invalid_client" in Spring Security when using private_key_jwt instead of client_secret_basic


I have tried to switch from client_secret_basic to private_key_jwt, but I'm getting the following error when I'm sent back from the auth provider: [invalid_client] Client authentication failed. No client authentication included

It's not a Spring Boot app, but this is what I have done so far:

private ClientRegistration idPortenClientRegistration() {
  return ClientRegistrations
    .fromIssuerLocation("the endpoint")
    .clientId("the client id")
    .registrationId("idporten")
    .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
    .redirectUri("the redirect url")
    .scope(Arrays.asList("the scopes"))
    .userNameAttributeName(IdTokenClaimNames.SUB)
    .clientName("idporten")
    .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT);
    .build();
}

My SecurityConfig.class:

http.oauth2Client(oauth2 -> oauth2
  .authorizationCodeGrant(codeGrant -> codeGrant
    .accessTokenResponseClient(accessTokenResponseClient())));

[…]

private DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient() {
  OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
  requestEntityConverter.addParametersConverter(
  new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver));

  DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
  tokenResponseClient.setRequestEntityConverter(requestEntityConverter);
  return tokenResponseClient;
}

private Function<ClientRegistration, JWK> jwkResolver = (clientRegistration) -> {
  if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
    JKSKeyManager keyManager = getApplicationContext().getBean("keyManager", JKSKeyManager.class);
    try {
      RSAPublicKey publicKey = (RSAPublicKey) keyManager.getPublicKey("idporten1");
      KeyStore.PrivateKeyEntry pkEntry = (KeyStore.PrivateKeyEntry) keyManager.getKeyStore()
        .getEntry("idporten1", new KeyStore.PasswordProtection(keyEntryPassword1.toCharArray()));
      RSAPrivateKey privateKey = (RSAPrivateKey) pkEntry.getPrivateKey();
      return new RSAKey.Builder(publicKey).privateKey(privateKey).keyID(UUID.randomUUID().toString()).build();
    } catch (NoSuchAlgorithmException | UnrecoverableEntryException | KeyStoreException e) {
      logger.error("Failed to configure jwkResolver: " + e.getMessage(), e);
    }
  }
  return null;
};

As mentioned, I'm successfully redirected to the auth provider, but getting the error described above when I'm sent back to the application. I have also tried to log accessTokenResponseClient() and jwkResolver. The former method gets called just before the error occurs, but nothing gets logged from the latter.

Some documentation from the provider: https://docs.digdir.no/oidc_protocol_token.html https://oidc-ver2.difi.no/idporten-oidc-provider/.well-known/openid-configuration


Solution

  • I figured it out. The NimbusJwtClientAuthenticationParametersConverter does not work at all. x5c and kid claims are just ignored, so I ended up with my own converter. And it is so small and simple, so I will share it here.

    But first of all – there was one mistake in my previous code, so the converter was never called. Here is what you have to add in the security config: http.oauth2Login() .tokenEndpoint().accessTokenResponseClient(accessTokenResponseClient());

    My simple converter just deals with a signed JWT and nothing else:

    public class SignedJwtClientAuthenticationParametersConverter<T extends AbstractOAuth2AuthorizationGrantRequest>
            implements Converter<T, MultiValueMap<String, String>> {
    
        private static final String CLIENT_ASSERTION_TYPE_VALUE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
    
        private final Function<ClientRegistration, String> jwt;
    
        public SignedJwtClientAuthenticationParametersConverter(
                final Function<ClientRegistration, String> jwt) {
            this.jwt = jwt;
        }
    
    
        @Override
        public MultiValueMap<String, String> convert(final T authorizationGrantRequest) {
            ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration();
    
            String jwt = this.jwt.apply(clientRegistration);
    
            MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
            parameters.set(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, CLIENT_ASSERTION_TYPE_VALUE);
            parameters.set(OAuth2ParameterNames.CLIENT_ASSERTION, jwt);
    
            return parameters;
        }
    }
    
    

    And here is the accessTokenResponseClient bean:

    @Bean
    public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
            OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
    
            requestEntityConverter.addParametersConverter(
                    new SignedJwtClientAuthenticationParametersConverter<>(jwkResolver));
    
            DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
            accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter);
            return accessTokenResponseClient;
        }
    
        private Function<ClientRegistration, String> jwkResolver = (clientRegistration) -> {
            if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
                try {
                    return getJwtByClientRegistration(clientRegistration);
                } catch (Exception e) {
                    logger.error("Failed to sign JWT: " + e.getMessage(), e);
                }
            }
            return null;
        };
    

    The method getJwtByClientRegistration() is now shown, but here I am using the JOSE library, which I am more familiar with.