Skip to content
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

Additional client_id field added in POST body for private_key_jwt authentication method for client credential grant type #11298

Open
Tracked by #15299
mit2222 opened this issue May 28, 2022 · 11 comments · May be fixed by #15339
Assignees
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) type: enhancement A general enhancement

Comments

@mit2222
Copy link

mit2222 commented May 28, 2022

Describe the bug
I specifically want to use WebClientReactiveClientCredentialsTokenResponseClient because it provides WebClient 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.

To Reproduce
I would like to retrieve access token via client_credentials private_key_jwt flow through Spring Boot WebClient in-memory solution.
Upon debugging, client_id gets added as a result of which the body consists client_assertion, client_assertion_type, scope,grant_type and client_id due to AbstractWebClientReactiveOAuth2AccessTokenResponseClient class populateTokenRequestBody() private method.

I tried to manually integrate with Okta v1/token url through postman with client_assertion value retrieved from jwksResolver and I do get a valid Bearer token.

Expected behavior
I get below error

{
    "errors": [
        {
            "status": "500",
            "title": "INTERNAL_SERVER_ERROR",
            "detail": "[invalid_request] Cannot supply multiple client credentials. Use one of the following: credentials in the Authorization header, credentials in the post body, or a client_assertion in the post body."
        }
    ]
}

I have the below bean configurations.

 @Bean
    ReactiveClientRegistrationRepository clientRegistrations() {

        ClientRegistration registration = ClientRegistration
                .withRegistrationId("OktaExample")
                .tokenUri("https://{oktaDomain}/oauth2/v1/token")
                .clientId(clientId)
                .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
                .scope(scope)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .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 -> {
                return new RSAKey.Builder(publicKey)
                        .privateKey(privateKey)
                        .build();
        };

        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();
    }
@mit2222 mit2222 added status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels May 28, 2022
@mit2222 mit2222 changed the title Multiple client_assertion appended for private_key_jwt authentication method for client credential grant type Additional client_id field added in POST body for private_key_jwt authentication method for client credential grant type May 28, 2022
@mit2222
Copy link
Author

mit2222 commented May 28, 2022

I was able to fix the issue by customizing WebClientReactiveClientCredentialsTokenResponseClient since client_id gets added in method populateTokenRequestBody(). Is there a cleaner solution than this ?

     if (!ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod()) && !ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
            body.with("client_id", clientRegistration.getClientId());
        }

@jgrandja jgrandja added in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) and removed type: bug A general bug labels May 30, 2022
@sjohnr
Copy link
Member

sjohnr commented Jun 2, 2022

@mit2222, thanks for reaching out!

I see the issue you're facing here. It does appear as though client_id is not an appropriate request parameter for this scenario, but it gets a bit complicated for a couple of reasons:

  1. Different specifications mix and match which parameters need to be present depending on the combination of grant type and client authentication method. The code you referenced probably worked fine until needing to add support for PRIVATE_KEY_JWT.
  2. Different providers may require and/or reject certain parameters which may not be called out in the spec and could require provider-specific customizations.
  3. Backwards compatibility is an issue here because these classes can be sub-classed by custom implementations.

Having said that, it's possible this could be addressed in 5.8 or 6.0 by changing the behavior of AbstractWebClientReactiveOAuth2AccessTokenResponseClient.populateTokenRequestBody() and the various subclasses in the framework. However, it's difficult to say whether this would have a large or only negligible effect on upgrading, as it could be a fairly unexpected breaking change if we changed the logic for populating client_id.

Was your workaround to copy the code from AbstractWebClientReactiveOAuth2AccessTokenResponseClient.populateTokenRequestBody() except the client_id in a custom subclass?

@sjohnr sjohnr added status: waiting-for-feedback We need additional information before we can continue type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged labels Jun 2, 2022
@MitCoder
Copy link

MitCoder commented Jun 3, 2022

@sjohnr I just created a custom class by copying contents from WebClientReactiveClientCredentialsTokenResponseClient as well as AbstractWebClientReactiveOAuth2AccessTokenResponseClient. In custom AbstractWebClientReactiveOAuth2AccessTokenResponseClient I just removed the portion from if class where client_id is added

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jun 3, 2022
@MitCoder
Copy link

MitCoder commented Jun 3, 2022

@sjohnr can you provide which dependency can I use whic has 5.8 and 6.0. I can probably try it out.
Also,how about a solution where I can override populateTokenRequestBody()

@sjohnr
Copy link
Member

sjohnr commented Jun 3, 2022

@MitCoder, 5.8 and 6.0 are unreleased, but you can clone the repo and look at the 5.8.x or main branches. As an alternate workaround, you can also try this (though it's a bit hacky perhaps):

Create the package org.springframework.security.oauth2.client.endpoint in your own application and create the following class:

public class CustomWebClientReactiveClientCredentialsTokenResponseClient
        extends WebClientReactiveClientCredentialsTokenResponseClient {
    @Override
    BodyInserters.FormInserter<String> populateTokenRequestBody(
            OAuth2ClientCredentialsGrantRequest grantRequest,
            BodyInserters.FormInserter<String> body) {

        ClientRegistration clientRegistration =
            ClientRegistration.withClientRegistration(clientRegistration(grantRequest))
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .build();

        OAuth2ClientCredentialsGrantRequest updatedGrantRequest =
            new OAuth2ClientCredentialsGrantRequest(clientRegistration);

        return super.populateTokenRequestBody(updatedGrantRequest, body);
    }
}

This works because the methods are package-private (default visibility) but not completely private. It creates a temporary ClientRegistration and OAuth2ClientCredentialsGrantRequest that suppress the client_id from being added in the base class. I think this will work for you but haven't tested it yet.

@jgrandja
Copy link
Contributor

jgrandja commented Jun 7, 2022

@mit2222 If client_id is not provided in the token request, then how does the provider (Okta) determine which public key to use to verify the Jwt client assertion?

The client registration at the provider (Okta) must contain metadata that contains the public key used to verify the Jwt client assertion. The public key may be registered with the client metadata or it may be exposed at the client application via a jwk-set-uri. Either way, the incoming token request must contain the client_id so the provider can locate the public key associated with the client via its metadata.

I suspect Okta does not require the client_id parameter because it's parsing the client_id from the Jwt sub claim. If this is the case, then parsing an untrusted (unverified) Jwt and using its claims is not secure since data integrity has not been confirmed at this point. Claims should only be used after the Jwt has been verified.

@sjohnr sjohnr added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels Jun 7, 2022
@spring-projects-issues
Copy link

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

@spring-projects-issues spring-projects-issues added the status: feedback-reminder We've sent a reminder that we need additional information before we can continue label Jun 14, 2022
@mit2222
Copy link
Author

mit2222 commented Jun 21, 2022

@jgrandja The client application which uses Signed JWT is created on Okta and it stores the public and private key. I use the same public and private key configuration in jwkResolver. I verified the jwk-set-uri and it doesnt have the public key that is being used in jwkResolver.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue status: feedback-reminder We've sent a reminder that we need additional information before we can continue labels Jun 21, 2022
@jgrandja
Copy link
Contributor

@mit2222 I tested this out with Okta and I was able to reproduce the following error:

"Cannot supply multiple client credentials. Use one of the following: credentials in the Authorization header, credentials in the post body, or a client_assertion in the post body."

Removing the client_id parameter resolved the issue.

As a temporary workaround, you can supply the following custom implementation of WebClientReactiveClientCredentialsTokenResponseClient:

public class CustomWebClientReactiveClientCredentialsTokenResponseClient
        extends WebClientReactiveClientCredentialsTokenResponseClient {
    @Override
    BodyInserters.FormInserter<String> populateTokenRequestBody(
            OAuth2ClientCredentialsGrantRequest grantRequest,
            BodyInserters.FormInserter<String> body) {

	Set<String> scopes = scopes(grantRequest);
	if (!CollectionUtils.isEmpty(scopes)) {
		body.with(OAuth2ParameterNames.SCOPE, StringUtils.collectionToDelimitedString(scopes, " "));
	}
	return body;
    }
}

As noted in @sjohnr comment, this custom implementation must reside in the package org.springframework.security.oauth2.client.endpoint so you can override the package-private populateTokenRequestBody().

@sjohnr We should consider adding a hook for customizing the default body parameters. Similar to setBodyExtractor() where the response can be handled/customized, maybe we add setBodyInserter() to allow customization of body parameters?

@sjohnr
Copy link
Member

sjohnr commented Apr 19, 2024

Related gh-14811

sjohnr added a commit to sjohnr/spring-security that referenced this issue Jul 1, 2024
This commit refactors and aligns usage of the parametersConverter in
AbstractWebClientReactiveOAuth2AccessTokenResponseClient with the same
capability in AbstractOAuth2AuthorizationGrantRequestEntityConverter
in order to align Reactive with Servlet for better consistency.

Closes spring-projectsgh-11298
@sjohnr
Copy link
Member

sjohnr commented Jul 1, 2024

Thanks for your patience on this issue. I've opened PR gh-15339 aimed at improving the situation here by allowing customization via setParametersConverter() (for Reactive ) to override parameters of the access token request (including conditionally omitting parameters like client_id), as in the following example:

@Configuration
@EnableWebFluxSecurity
public class SecurityConfiguration {

	private RSAPublicKey publicKey;

	private RSAPrivateKey privateKey;

	// ...

	@Bean
	public ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
		var accessTokenResponseClient = new WebClientReactiveClientCredentialsTokenResponseClient();
		accessTokenResponseClient.setParametersConverter(defaultParametersConverter());
		accessTokenResponseClient.addParametersConverter(jwtClientAuthenticationParametersConverter());

		return accessTokenResponseClient;
	}

	private Converter<OAuth2ClientCredentialsGrantRequest, MultiValueMap<String, String>> defaultParametersConverter() {
		return (grantRequest) -> {
			var clientRegistration = grantRequest.getClientRegistration();
			var parameters = new LinkedMultiValueMap<String, String>();
			parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue());
			if (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientRegistration.getClientAuthenticationMethod())) {
				parameters.set(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
				parameters.set(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
			}
			if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) {
				var scopes = StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), " ");
				parameters.set(OAuth2ParameterNames.SCOPE, scopes);
			}

			return parameters;
		};
	}

	private Converter<OAuth2ClientCredentialsGrantRequest, MultiValueMap<String, String>> jwtClientAuthenticationParametersConverter() {
		return new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver());
	}

	private Function<ClientRegistration, JWK> jwkResolver() {
		return (clientRegistration) -> new RSAKey.Builder(this.publicKey)
			.privateKey(this.privateKey)
			.build();
	}

}

This is currently not possible due to the internal structure of the abstract base class. I've refactored things to allow parameter overrides (without using the package-collision workaround to create a new subclass). Feel free to take a look and provide feedback.

sjohnr added a commit to sjohnr/spring-security that referenced this issue Jul 2, 2024
This commit refactors and aligns usage of the parametersConverter in
AbstractWebClientReactiveOAuth2AccessTokenResponseClient with the same
capability in AbstractOAuth2AuthorizationGrantRequestEntityConverter
in order to align Reactive with Servlet for better consistency.

Closes spring-projectsgh-11298
sjohnr added a commit that referenced this issue Sep 19, 2024
sjohnr added a commit to sjohnr/spring-security that referenced this issue Sep 19, 2024
This commit refactors and aligns usage of the parametersConverter in
AbstractWebClientReactiveOAuth2AccessTokenResponseClient with the same
capability in AbstractOAuth2AuthorizationGrantRequestEntityConverter
in order to align Reactive with Servlet for better consistency.

Issue spring-projectsgh-11298
sjohnr added a commit to sjohnr/spring-security that referenced this issue Sep 19, 2024
sjohnr added a commit to sjohnr/spring-security that referenced this issue Sep 20, 2024
sjohnr added a commit to sjohnr/spring-security that referenced this issue Sep 20, 2024
sjohnr added a commit to sjohnr/spring-security that referenced this issue Sep 23, 2024
sjohnr added a commit to sjohnr/spring-security that referenced this issue Sep 27, 2024
This commit refactors and aligns usage of the parametersConverter in
AbstractWebClientReactiveOAuth2AccessTokenResponseClient with the same
capability in AbstractOAuth2AuthorizationGrantRequestEntityConverter
in order to align Reactive with Servlet for better consistency.

Issue spring-projectsgh-11298
sjohnr added a commit to sjohnr/spring-security that referenced this issue Sep 27, 2024
sjohnr added a commit to sjohnr/spring-security that referenced this issue Sep 27, 2024
sjohnr added a commit to sjohnr/spring-security that referenced this issue Sep 27, 2024
sjohnr added a commit to sjohnr/spring-security that referenced this issue Sep 30, 2024
This commit refactors and aligns usage of the parametersConverter in
AbstractWebClientReactiveOAuth2AccessTokenResponseClient with the same
capability in AbstractOAuth2AuthorizationGrantRequestEntityConverter
in order to align Reactive with Servlet for better consistency.

Issue spring-projectsgh-11298
sjohnr added a commit to sjohnr/spring-security that referenced this issue Sep 30, 2024
sjohnr added a commit to sjohnr/spring-security that referenced this issue Sep 30, 2024
sjohnr added a commit to sjohnr/spring-security that referenced this issue Sep 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) type: enhancement A general enhancement
Projects
Status: In Progress
Development

Successfully merging a pull request may close this issue.

5 participants