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();
}
Comment From: mit2222
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());
}
Comment From: sjohnr
@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:
- 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. - Different providers may require and/or reject certain parameters which may not be called out in the spec and could require provider-specific customizations.
- 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?
Comment From: MitCoder
@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
Comment From: MitCoder
@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()
Comment From: sjohnr
@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.
Comment From: jgrandja
@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.
Comment From: spring-projects-issues
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.
Comment From: mit2222
@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.
Comment From: jgrandja
@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?
Comment From: sjohnr
Related gh-14811
Comment From: sjohnr
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.