Skip to content

Add possibility to insert extra form data parameter when getting access token with oauth2 client_credential flow #7781

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

Closed
jpiccaluga opened this issue Jan 2, 2020 · 10 comments
Assignees
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: declined A suggestion or change that we don't feel we should currently apply

Comments

@jpiccaluga
Copy link

jpiccaluga commented Jan 2, 2020

Summary

Some OIDC/OAuth2 provider like Auth0 requires audience parameter set in the post body when calling /oauth/token endpoint to retrieve access token with the client_credentials flow. They do this because an machine to machine client could potentially get access to multiple api resource server.
In order to make client_credentials work with OAuth2 provider specificity we need to re-implement a custom OAuth2AccessTokenResponseClient or ReactiveOAuth2AccessTokenResponseClient to include the missing require field (audience or what ever provider specificity).

It will be very appreciate if we can add free extra parameters to the request body when calling token endpoint. For example we can add an extra property as a Map<String, String> in the ClientRegistration class and add it to the body on OAuth2ClientCredentialsGrantRequestEntityConverter and WebClientReactiveClientCredentialsTokenResponseClient.

Actual Behavior

When trying client_credential flow with Auth0 I got 403 could not retrieve token because the audience parameter is not specified in the request.

Expected Behavior

Get status code 200 and retrieve access token form Auth0 provider

Configuration

application.yml

spring:
  application:
    name: user-mangement
  security:
    oauth2:
      client:
        provider:
          oauth0Management:
            token-uri: https://<redacted>/oauth/token
        registration:
          oauth0Management:
            client-authentication-method: POST
            authorization-grant-type: client_credentials
            client-id: <redacted>
            client-secret: <redacted>
            scope: read:users
@Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations) {
    ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
      new ServerOAuth2AuthorizedClientExchangeFilterFunction(
        clientRegistrations,
        new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
    oauth.setDefaultClientRegistrationId("oauth0Management");
    return WebClient.builder()
      .filter(oauth)
      .build();
}

Version

spring-security -> 5.2.1

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jan 2, 2020
@jpiccaluga jpiccaluga changed the title Add possibility to insert extra body parameter in WebClientReactiveClientCredentialsTokenResponseClient Add possibility to insert extra body parameter when getting access token with oauth2 client_credential flow Jan 2, 2020
@jpiccaluga jpiccaluga changed the title Add possibility to insert extra body parameter when getting access token with oauth2 client_credential flow Add possibility to insert extra data form parameter when getting access token with oauth2 client_credential flow Jan 4, 2020
@jpiccaluga jpiccaluga changed the title Add possibility to insert extra data form parameter when getting access token with oauth2 client_credential flow Add possibility to insert extra form data parameter when getting access token with oauth2 client_credential flow Jan 4, 2020
@jgrandja
Copy link
Contributor

jgrandja commented Jan 7, 2020

@jpiccaluga It is already possible to customize the Access Token request. Take a look at the reference doc where you can specify a custom DefaultClientCredentialsTokenResponseClient.setRequestEntityConverter().

For the reactive side, you can supply a custom WebClient to WebClientReactiveClientCredentialsTokenResponseClient.setWebClient() configured with an ExchangeFilterFunction that could modify the request by adding the additional parameter(s). See this comment for an example. NOTE: The example performs response post-processing so in your case you would use ExchangeFilterFunction.ofRequestProcessor() for request pre-processing.

I'm going to close this issue since this capability is already available. Please let me know if I missed something.

@jgrandja jgrandja closed this as completed Jan 7, 2020
@jgrandja jgrandja added in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) and removed status: waiting-for-triage An issue we've not yet triaged labels Jan 7, 2020
@jgrandja jgrandja self-assigned this Jan 7, 2020
@jpiccaluga
Copy link
Author

jpiccaluga commented Jan 8, 2020

@jgrandja Thanks for your response.
My concern with this approach is that we have to split the configuration. With spring boot it will give something like this.

spring:
  application:
    name: my-app
  security:
    oauth2:
      client:
        provider:
          oauth0Management:
            token-uri: https://<redacted>/oauth/token
        registration:
          oauth0Management:
            client-authentication-method: POST
            authorization-grant-type: client_credentials
            client-id: <redacted>
            client-secret: <redacted>
            scope: read:users
          anOtherClient:
            client-authentication-method: POST
              authorization-grant-type: client_credentials
              client-id: <redacted>
              client-secret: <redacted>
              scope: read:users

registration-ext:
  oauth0Management:
    audience: "http://test/api"
    whatever: "toto"
  anOtherClient:
    audience: "http://test/api"

This is not very elegant.

Something like this is preferable:

spring:
  application:
    name: my-app
  security:
    oauth2:
      client:
        provider:
          oauth0Management:
            token-uri: https://<redacted>/oauth/token
        registration:
          oauth0Management:
            client-authentication-method: POST
            authorization-grant-type: client_credentials
            client-id: <redacted>
            client-secret: <redacted>
            scope: read:users
            additional-form-data:
              audience: "http://test/api"
              whatever: "toto"
          anOtherClient:
            client-authentication-method: POST
              authorization-grant-type: client_credentials
              client-id: <redacted>
              client-secret: <redacted>
              scope: read:users
              additional-form-data:
                audience: "http://test/api"
                whatever: "toto"

What do you think?

@jpiccaluga
Copy link
Author

This is not a lot of code to change. If you agree on that it will be more readable and easier for a developer to understand how to enrich the form data, I can submit you a PR.
Let me know.
Cheers

@jgrandja
Copy link
Contributor

@jpiccaluga

Given the proposed additional configuration:

additional-form-data:
              audience: "http://test/api"
              whatever: "toto"

Which specific endpoint is posted additional-form-data? This name is too generic as there are multiple points where requests are made via the client protocols, eg. Authorization Request, Token Request, and UserInfo request. There are a few others as well.

The biggest issue with this configuration approach is that the form data is static. This is a huge drawback when you need to derive form data from the environment/application in a dynamic way.

Furthermore, adding this type of application configuration causes unnecessary complexity to the yaml. I like to keep things simple and clean so this type of data - custom form parameters - is an advanced customization and is recommended to customize via the specific feature component.

@jpiccaluga
Copy link
Author

@jgrandja
I agree that it's too generic a better name would be:

token-endpoint-additional-form-data:
  audience: "http://test/api"
  whatever: "toto"

This yaml configuration should be applied in OAuth2ClientProperties.java of spring boot project here and at this level I didn't see which kind of extra complexity it will add.

At the spring-security level, we only speak of adding Map<String, String> tokenEndpointAdditionalFormData = Collections.emptyMap() property to ClientRegistration:

public final class ClientRegistration implements Serializable {
	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
	private String registrationId;
	private String clientId;
	private String clientSecret;
	private ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.BASIC;
	private AuthorizationGrantType authorizationGrantType;
	private String redirectUriTemplate;
	private Set<String> scopes = Collections.emptySet();
	private ProviderDetails providerDetails = new ProviderDetails();
	private String clientName;
	private Map<String, String> tokenEndpointAdditionalFormData = Collections.emptyMap();
...

And adding this line to WebClientReactiveClientCredentialsTokenResponseClient#body method:

	private static BodyInserters.FormInserter<String> body(OAuth2ClientCredentialsGrantRequest authorizationGrantRequest) {
		ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration();
		BodyInserters.FormInserter<String> body = BodyInserters
				.fromFormData(OAuth2ParameterNames.GRANT_TYPE, authorizationGrantRequest.getGrantType().getValue());
		Set<String> scopes = clientRegistration.getScopes();
		if (!CollectionUtils.isEmpty(scopes)) {
			String scope = StringUtils.collectionToDelimitedString(scopes, " ");
			body.with(OAuth2ParameterNames.SCOPE, scope);
		}
		if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
			body.with(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
			body.with(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
			clientRegistration.getTokenEndpointAdditionalFormData().forEach(body::with);
		}
		return body;
	}

At this level I didn't see drawback when you need to derive form data from the environment/application in a dynamic way. Could you elaborate a little bit I am not sure to catch this point.

IMHO keep it at configuration is much more intelligible than having one piece in the configuration and an other in an ExchangeFilterFunction.

@jgrandja
Copy link
Contributor

@jpiccaluga

Not all custom form parameters are static in nature. Some of the custom form data that is required for an endpoint may need to be derived dynamically by the application. This is a very common use case. With the application property approach, you could only configure static data which is quite limiting.

The current ability to add custom parameters in either the Authorization Request, Token Request or UserInfo request provides all the flexibility needed to enhance the request with any data - dynamic or static.

@jgrandja jgrandja added the status: declined A suggestion or change that we don't feel we should currently apply label Jan 15, 2020
@jpiccaluga
Copy link
Author

First of all application properties is related to spring boot here.
As you can see this approach is already mainly used.

Secondary in spring security repo I only suggest to modifiy ClientRegistration class and it do not remove you the ability to do add other field dynamically.

Finally the implementation of token endpoint from oidc/oauth2 provider to provider is very static. I means request parameter that you should provide for a specific provider will likely never change.

What do you think? Could you reconsider this feature request?

@blatobi
Copy link

blatobi commented Jan 26, 2021

@jgrandja

For the reactive side, you can supply a custom WebClient to WebClientReactiveClientCredentialsTokenResponseClient.setWebClient() configured with an ExchangeFilterFunction that could modify the request by adding the additional parameter(s). See this comment for an example. NOTE: The example performs response post-processing so in your case you would use ExchangeFilterFunction.ofRequestProcessor() for request pre-processing.

Sorry for asking on this old thread.
I don't see how adding a WebClient with a filter is helping here. Looking add AbstractWebClientReactiveOAuth2AccessTokenResponseClient all important values are part of the request-body that is created:

      private BodyInserters.FormInserter<String> createTokenRequestBody(T grantRequest) {
		BodyInserters.FormInserter<String> body = BodyInserters.fromFormData(OAuth2ParameterNames.GRANT_TYPE,
				grantRequest.getGrantType().getValue());
		return populateTokenRequestBody(grantRequest, body);
	}

	BodyInserters.FormInserter<String> populateTokenRequestBody(T grantRequest,
			BodyInserters.FormInserter<String> body) {
		ClientRegistration clientRegistration = clientRegistration(grantRequest);
		if (!ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
			body.with(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
		}
		if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
			body.with(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
		}
		Set<String> scopes = scopes(grantRequest);
		if (!CollectionUtils.isEmpty(scopes)) {
			body.with(OAuth2ParameterNames.SCOPE, StringUtils.collectionToDelimitedString(scopes, " "));
		}
		return body;
	}

The audience that is referenced by @jpiccaluga from opening this issue is required to be part of the body.

How could the body be manipulated within a filter so it contains additionally the audience?

The only way I can see is completely copying the implementation of AbstractWebClientReactiveOAuth2AccessTokenResponseClient as it does not allow customizing the body.

@blatobi
Copy link

blatobi commented Jan 28, 2021

Found the answer in issue 9171:

private ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsTokenResponseClient() {
	WebClient webClient = WebClient.builder().filter(clientCredentialsTokenRequestProcessor()).build();
	WebClientReactiveClientCredentialsTokenResponseClient clientCredentialsTokenResponseClient =
			new WebClientReactiveClientCredentialsTokenResponseClient();
	clientCredentialsTokenResponseClient.setWebClient(webClient);
	return clientCredentialsTokenResponseClient;
}

private static ExchangeFilterFunction clientCredentialsTokenRequestProcessor() {
	return ExchangeFilterFunction.ofRequestProcessor(request ->
			Mono.just(ClientRequest.from(request)
					.body(((BodyInserters.FormInserter<String>) request.body())
							.with("resource", "resource1"))
					.build()
			)
	);
}

@shubham-patidar-roostify

Found the answer in issue 9171:

private ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsTokenResponseClient() {
	WebClient webClient = WebClient.builder().filter(clientCredentialsTokenRequestProcessor()).build();
	WebClientReactiveClientCredentialsTokenResponseClient clientCredentialsTokenResponseClient =
			new WebClientReactiveClientCredentialsTokenResponseClient();
	clientCredentialsTokenResponseClient.setWebClient(webClient);
	return clientCredentialsTokenResponseClient;
}

private static ExchangeFilterFunction clientCredentialsTokenRequestProcessor() {
	return ExchangeFilterFunction.ofRequestProcessor(request ->
			Mono.just(ClientRequest.from(request)
					.body(((BodyInserters.FormInserter<String>) request.body())
							.with("resource", "resource1"))
					.build()
			)
	);
}

@blatobi How can we use this in our code? Can you please provide little more details as to where we've use the clientCredentialsTokenResponseClient() method ?

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) status: declined A suggestion or change that we don't feel we should currently apply
Projects
None yet
Development

No branches or pull requests

5 participants