Describe the bug I am using spring-boot-starter-parent 2.6.4. I need to integrate with Okta api with client credentials private_key_jwt. Okta's /v1/token url needs client_assertion_type of urn:ietf:params:oauth:client-assertion-type:jwt-bearer, grant type as client_credentials and authentication method as PRIVATE_KEY_JWT.

Expected behavior I would like to retrieve access token via client_credentials private_key_jwt flow through Spring Boot WebClient in-memory solution.

To Reproduce I have the below bean configurations. However, v1/token url never gets called. I think the issue with client_assertion_type urn:ietf:params:oauth:client-assertion-type:jwt-bearer along with clientAuthenticationMethod as PRIVATE_KEY_JWT for grant type as client_credentials.


 @Bean
    ReactiveClientRegistrationRepository clientRegistrations() {

        ClientRegistration registration = ClientRegistration
                .withRegistrationId("OktaExample")
                .tokenUri("https://{oktaDomain}/oauth2/v1/token")
                .clientId(clientId)
                .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
                .scope(scope)
                .authorizationGrantType(new AuthorizationGrantType("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"))
                .build();
        return new InMemoryReactiveClientRegistrationRepository(registration);
    }


    @Bean
    public ReactiveOAuth2AuthorizedClientService authorizedClientService(
            ReactiveClientRegistrationRepository clientRegistrations) {
        return new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrations);
    }
    @Bean
    public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
            ReactiveClientRegistrationRepository clientRegistrations,
            ReactiveOAuth2AuthorizedClientService authorizedClientService) {
        return configureHttpProxy(
                new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
                        clientRegistrations,
                        authorizedClientService
                ));
    }

  private AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager configureHttpProxy(AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager) {

        Function<ClientRegistration, JWK> jwkResolver = client -> {
                // Assuming RSA key type
                return new RSAKey.Builder(publicKey)
                        .privateKey(privateKey)
                        .keyID(UUID.randomUUID().toString())
                        .build();
        };

        // set the webclient with proxy configuration in the ReactiveOAuth2AccessTokenResponseClient
        WebClientReactiveClientCredentialsTokenResponseClient tokenResponseClient = new WebClientReactiveClientCredentialsTokenResponseClient();
        tokenResponseClient.addParametersConverter(new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver));

        tokenResponseClient.setWebClient(
                WebClient.builder().build()
        );

        ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider= ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                .clientCredentials(clientCredentialsGrantBuilder ->
                        clientCredentialsGrantBuilder.accessTokenResponseClient(tokenResponseClient)) 
                .build();


        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }
   @Bean
    WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {


       ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        oauth2Client.setDefaultClientRegistrationId("OktaExample");

        return WebClient.builder()
                .filters(exchangeFilterFunctions -> {
                    exchangeFilterFunctions.add(oauth2Client);
                })
                .build();
    }

Comment From: jgrandja

@mit2222 The above configuration is incomplete. The provider (Okta) needs to be configured to fetch the public key from the client application in order to verify the signature in the (Jwt) client assertion.

Please see this working sample that uses Spring Security's OAuth client support integrated with Spring Authorization Server as the provider. The client_credentials grant with client authentication urn:ietf:params:oauth:client-assertion-type:jwt-bearer is configured. You will see the JwkSetController that exposes the public key in the client application, which the provider will fetch and verify.

I'm going to close this since this feature is working as per sample.

Comment From: mit2222

Thank you so much @jgrandja for the quick response . I referred your working sample and I was able to get an access token based on private_key_jwt ClientAuthenticationMethod with client_credentials grant type and client_assertion_type as urn:ietf:params:oauth:client-assertion-type:jwt-bearer. I have the below bean configurations.


@Bean
    ClientRegistrationRepository clientRegistrationRepository() {

        ClientRegistration registration = ClientRegistration
                .withRegistrationId("OktaExample")
                .tokenUri("https://{oktaDomain}/oauth2/v1/token")
                .clientId(clientId)
                .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .scope(scope)
                .jwkSetUri("https://{oktaDomain}/oauth2/v1/keys")
                .build();
        return new InMemoryClientRegistrationRepository(registration);
    }

  @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientRepository authorizedClientRepository) {
            OAuth2AuthorizedClientProvider authorizedClientProvider =
                    OAuth2AuthorizedClientProviderBuilder.builder()
                            .authorizationCode()
                            .clientCredentials(clientCredentials -> clientCredentials.accessTokenResponseClient(clientCredentialsTokenResponseClient()))
                            .build();
        DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
                clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }


    @Bean
    OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsTokenResponseClient() {

        Function<ClientRegistration, JWK> jwkResolver = client -> {
            return new RSAKey.Builder(publicKey)
                    .privateKey(privateKey)
                    .keyID("kidValue")
                    .build();
        };

        OAuth2ClientCredentialsGrantRequestEntityConverter clientCredentialsGrantRequestEntityConverter =
                new OAuth2ClientCredentialsGrantRequestEntityConverter();
        clientCredentialsGrantRequestEntityConverter.addParametersConverter(
                new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver));


        DefaultClientCredentialsTokenResponseClient clientCredentialsTokenResponseClient =
                new DefaultClientCredentialsTokenResponseClient();
        clientCredentialsTokenResponseClient.setRequestEntityConverter(clientCredentialsGrantRequestEntityConverter);

        return clientCredentialsTokenResponseClient;
    }


   @Bean
    WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {

        ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
                new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        oauth2Client.setDefaultClientRegistrationId("OktaExample");

        return WebClient.builder()
                .filters(exchangeFilterFunctions -> {
                    exchangeFilterFunctions.add(oauth2Client);
                })
                .apply(oauth2Client.oauth2Configuration())
                .clientConnector(connector)
                .build();
    }

Comment From: jdi77

@mit2222 do you have a github link for the above fix?

Comment From: MitCoder

No,I don’t have the git link.