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 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

Comment From: jgrandja

@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.

Comment From: jpiccaluga

@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?

Comment From: jpiccaluga

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

Comment From: jgrandja

@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.

Comment From: jpiccaluga

@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.

Comment From: jgrandja

@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.

Comment From: jpiccaluga

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?

Comment From: blatobi

@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.

Comment From: blatobi

Found the answer in issue 9171:

```java private ReactiveOAuth2AccessTokenResponseClient 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) request.body()) .with("resource", "resource1")) .build() ) ); } ```

Comment From: shubham-patidar-roostify

Found the answer in issue 9171:

```java private ReactiveOAuth2AccessTokenResponseClient 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) 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 ?