14

I am trying to implement the client_credentials grant to get a token in my spring boot resource server. I am using Auth0 as an Authorization server. They seem to require an extra parameter in the request body to be added called audience.

I have tried to do the request through postman and it works. I am now trying to reproduce it within Spring. Here is the working postman request

curl -X POST \
  https://XXX.auth0.com/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials&audience=https%3A%2F%2Fxxxxx.auth0.com%2Fapi%2Fv2%2F&client_id=SOME_CLIENT_ID&client_secret=SOME_CLIENT_SECRET'

The problem I am facing is that i have no way to add the missing audience parameter to the token request.

I have a configuration defined in my application.yml

client:
    provider:
      auth0:
        issuer-uri: https://XXXX.auth0.com//
    registration:
      auth0-client:
        provider: auth0
        client-id: Client
        client-secret: Secret
        authorization_grant_type: client_credentials
      auth0:
        client-id: Client
        client-secret: Secret

I have the web client filter configured like this.

@Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations,
                    ServerOAuth2AuthorizedClientRepository authorizedClients) {
    ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
            clientRegistrations, authorizedClients);
    oauth2.setDefaultClientRegistrationId("auth0");
    return WebClient.builder()
            .filter(oauth2)
            .build();
}

I am injecting the instance and trying to do a request to get the user by email

 return this.webClient.get()
            .uri(this.usersUrl + "/api/v2/users-by-email?email={email}", email)
            .attributes(auth0ClientCredentials())
            .retrieve()
            .bodyToMono(User.class);

The way i understand it, the filter intercepts this userByEmail request and before it executes it it tries to execute the /oauth/token request to get JWT Bearer token which it can append to the first one and execute it.

Is there a way to add a parameter to the filter? It has been extremely difficult to step through it and figure out where exactly the parameters are being appended since its reactive and am quite new at this. Even some pointers to where to look would be helpful.

DArkO
  • 15,880
  • 12
  • 60
  • 88

4 Answers4

5

I was having the same problem where access token response and request for it wasn't following oAuth2 standards. Here's my code (it's in kotlin but should be understandable also for java devs) for spring boot version 2.3.6.RELEASE. Gradle dependencies:

implementation(enforcedPlatform("org.springframework.boot:spring-boot-dependencies:${springBootVersion}"))
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")

After adding them you have to firstly create your custom token request/response client which will implement ReactiveOAuth2AccessTokenResponseClient interface:

class CustomTokenResponseClient : ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {

    private val webClient = WebClient.builder().build()

    override fun getTokenResponse(
            authorizationGrantRequest: OAuth2ClientCredentialsGrantRequest
    ): Mono<OAuth2AccessTokenResponse> =
            webClient.post()
                    .uri(authorizationGrantRequest.clientRegistration.providerDetails.tokenUri)
                    .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                    .bodyValue(CustomTokenRequest(
                            clientId = authorizationGrantRequest.clientRegistration.clientId,
                            clientSecret = authorizationGrantRequest.clientRegistration.clientSecret
                    ))
                    .exchange()
                    .flatMap { it.bodyToMono<NotStandardTokenResponse>() }
                    .map { it.toOAuth2AccessTokenResponse() }


    private fun NotStandardTokenResponse.toOAuth2AccessTokenResponse() = OAuth2AccessTokenResponse
            .withToken(this.accessToken)
            .refreshToken(this.refreshToken)
            .expiresIn(convertExpirationDateToDuration(this.data.expires).toSeconds())
            .tokenType(OAuth2AccessToken.TokenType.BEARER)
            .build()

}

As you can see above, in this class you can adjust token request/response handling to your specific needs.

Note: authorizationGrantRequest param inside getTokenResponse method. Spring is passing here data from you application properties, so follow the standards when defining them, e.g. they may look like this:

spring:
  security:
    oauth2:
      client:
        registration:
          name-for-oauth-integration:
            authorization-grant-type: client_credentials
            client-id: id
            client-secret: secret
        provider:
          name-for-oauth-integration:
            token-uri: https://oauth.com/token

The last step is to use your CustomTokenResponseClient inside oAuth2 configuration, it may look like this:

@Configuration
class CustomOAuth2Configuration {

    @Bean
    fun customOAuth2WebWebClient(clientRegistrations: ReactiveClientRegistrationRepository): WebClient {
        val clientRegistryRepo = InMemoryReactiveClientRegistrationRepository(
                clientRegistrations.findByRegistrationId("name-for-oauth-integration").block()
        )
        val clientService = InMemoryReactiveOAuth2AuthorizedClientService(clientRegistryRepo)

        val authorizedClientManager =
                AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistryRepo, clientService)
        val authorizedClientProvider = ClientCredentialsReactiveOAuth2AuthorizedClientProvider()
        authorizedClientProvider.setAccessTokenResponseClient(CustomTokenResponseClient())
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)

        val oauthFilter = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
        oauthFilter.setDefaultClientRegistrationId("name-for-oauth-integration")

        return WebClient.builder()
                .filter(oauthFilter)
                .build()
    }

}
Adrian Kapuscinski
  • 1,124
  • 12
  • 13
2

Right now, this is possible, but not elegant.

Note that you can provide a custom ReactiveOAuth2AccessTokenResponseClient to ServerOAuth2AuthorizedClientExchangeFilterFunction.

You can create your own implementation of this - and thereby add any other parameters you need - by copying the contents of WebClientReactiveClientCredentialsTokenResponseClient.

That said, it would be better if there were a setter to make that more convenient. You can follow the corresponding issue in Spring Security's backlog.

jzheaux
  • 7,042
  • 3
  • 22
  • 36
  • Thank you for taking the time to answer my question. This was very helpful in figuring out how to solve my problem. – DArkO Nov 30 '18 at 13:48
2

Here is what i found out after further investigation. The code described in my question was never going to call the client_credentials and fit my use-case. I think (not 100% sure on this) it will be very useful in the future if i am trying to propagate the user submitted token around multiple services in a micro-service architecture. A chain of actions like this comes to mind:

User calls Service A -> Service A calls Service B -> Service B responds -> Service A responds back to user request.

And using the same token to begin with through the whole process.

My solution to my use-case:

What i did was create a new Filter class largely based on the original and implement a step before executing the request where i check if i have a JWT token stored that can be used for the Auth0 Management API. If i don't i build up the client_credentials grant request and get one, then attach this token as a bearer to the initial request and execute that one. I also added a small token in-memory caching mechanism so that if the token is valid any other requests at a later date will just use it. Here is my code.

Filter

public class Auth0ClientCredentialsGrantFilterFunction implements ExchangeFilterFunction {

    private ReactiveClientRegistrationRepository clientRegistrationRepository;

    /**
     * Required by auth0 when requesting a client credentials token
     */
    private String audience;

    private String clientRegistrationId;

    private Auth0InMemoryAccessTokenStore auth0InMemoryAccessTokenStore;

    public Auth0ClientCredentialsGrantFilterFunction(ReactiveClientRegistrationRepository clientRegistrationRepository,
                                                     String clientRegistrationId,
                                                     String audience) {
        this.clientRegistrationRepository = clientRegistrationRepository;
        this.audience = audience;
        this.clientRegistrationId = clientRegistrationId;
        this.auth0InMemoryAccessTokenStore = new Auth0InMemoryAccessTokenStore();
    }

    public void setAuth0InMemoryAccessTokenStore(Auth0InMemoryAccessTokenStore auth0InMemoryAccessTokenStore) {
        this.auth0InMemoryAccessTokenStore = auth0InMemoryAccessTokenStore;
    }

    @Override
    public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
        return auth0ClientCredentialsToken(next)
                .map(token -> bearer(request, token.getTokenValue()))
                .flatMap(next::exchange)
                .switchIfEmpty(next.exchange(request));
    }

    private Mono<OAuth2AccessToken> auth0ClientCredentialsToken(ExchangeFunction next) {
        return Mono.defer(this::loadClientRegistration)
                .map(clientRegistration -> new ClientCredentialsRequest(clientRegistration, audience))
                .flatMap(request -> this.auth0InMemoryAccessTokenStore.retrieveToken()
                        .switchIfEmpty(refreshAuth0Token(request, next)));
    }

    private Mono<OAuth2AccessToken> refreshAuth0Token(ClientCredentialsRequest clientCredentialsRequest, ExchangeFunction next) {
        ClientRegistration clientRegistration = clientCredentialsRequest.getClientRegistration();
        String tokenUri = clientRegistration
                .getProviderDetails().getTokenUri();
        ClientRequest clientCredentialsTokenRequest = ClientRequest.create(HttpMethod.POST, URI.create(tokenUri))
                .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
                .body(clientCredentialsTokenBody(clientCredentialsRequest))
                .build();
        return next.exchange(clientCredentialsTokenRequest)
                .flatMap(response -> response.body(oauth2AccessTokenResponse()))
                .map(OAuth2AccessTokenResponse::getAccessToken)
                .doOnNext(token -> this.auth0InMemoryAccessTokenStore.storeToken(token));
    }

    private static BodyInserters.FormInserter<String> clientCredentialsTokenBody(ClientCredentialsRequest clientCredentialsRequest) {
        ClientRegistration clientRegistration = clientCredentialsRequest.getClientRegistration();
        return BodyInserters
                .fromFormData("grant_type", AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
                .with("client_id", clientRegistration.getClientId())
                .with("client_secret", clientRegistration.getClientSecret())
                .with("audience", clientCredentialsRequest.getAudience());
    }

    private Mono<ClientRegistration> loadClientRegistration() {
        return Mono.just(clientRegistrationId)
                .flatMap(r -> clientRegistrationRepository.findByRegistrationId(r));
    }

    private ClientRequest bearer(ClientRequest request, String token) {
        return ClientRequest.from(request)
                .headers(headers -> headers.setBearerAuth(token))
                .build();
    }


    static class ClientCredentialsRequest {
        private final ClientRegistration clientRegistration;
        private final String audience;

        public ClientCredentialsRequest(ClientRegistration clientRegistration, String audience) {
            this.clientRegistration = clientRegistration;
            this.audience = audience;
        }

        public ClientRegistration getClientRegistration() {
            return clientRegistration;
        }

        public String getAudience() {
            return audience;
        }
    }

}

Token Store

public class Auth0InMemoryAccessTokenStore implements ReactiveInMemoryAccessTokenStore {

    private AtomicReference<OAuth2AccessToken> token = new AtomicReference<>();
    private Clock clock = Clock.systemUTC();
    private Duration accessTokenExpiresSkew = Duration.ofMinutes(1);

    public Auth0InMemoryAccessTokenStore() {
    }

    @Override
    public Mono<OAuth2AccessToken> retrieveToken() {
        return Mono.justOrEmpty(token.get())
                .filter(Objects::nonNull)
                .filter(token -> token.getExpiresAt() != null)
                .filter(token -> {
                    Instant now = this.clock.instant();
                    Instant expiresAt = token.getExpiresAt();
                    if (now.isBefore(expiresAt.minus(this.accessTokenExpiresSkew))) {
                        return true;
                    }
                    return false;
                });
    }

    @Override
    public Mono<Void> storeToken(OAuth2AccessToken token) {
        this.token.set(token);
        return Mono.empty();
    }
}

Token Store Interface

public interface ReactiveInMemoryAccessTokenStore {
    Mono<OAuth2AccessToken> retrieveToken();

    Mono<Void> storeToken(OAuth2AccessToken token);
}

And finally defining the beans and using it.

    @Bean
    public Auth0ClientCredentialsGrantFilterFunction auth0FilterFunction(ReactiveClientRegistrationRepository clientRegistrations,
                                                                         @Value("${auth0.client-registration-id}") String clientRegistrationId,
                                                                         @Value("${auth0.audience}") String audience) {
        return new Auth0ClientCredentialsGrantFilterFunction(clientRegistrations, clientRegistrationId, audience);
    }

    @Bean(name = "auth0-webclient")
    WebClient webClient(Auth0ClientCredentialsGrantFilterFunction filter) {
        return WebClient.builder()
                .filter(filter)
                .build();
    }

There is a slight problem with the token store at this time as the client_credentials token request will be executed multiple on parallel requests that come at the same time, but i can live with that for the foreseeable future.

DArkO
  • 15,880
  • 12
  • 60
  • 88
  • This was very useful for me recently. Do you happen to know if there is an updated solution to the original problem? – FakeSheikh Aug 02 '22 at 13:53
  • Feel free to post a solution if you find a better one. For me this works still. I suppose there should be something out-of-the box which helps you get a webclient with the correct token for auth0. – DArkO Aug 03 '22 at 15:11
0

Your application.yml is missing one variable: client-authentication-method: post
it should be like this:

spring:
 security:
  oauth2:
   client:
    provider:
     auth0-client:
      token-uri: https://XXXX.auth0.com//
    registration:
     auth0-client:
      client-id: Client
      client-secret: Secret
      authorization_grant_type: client_credentials
      client-authentication-method: post

Without it I was getting "invalid_client" response all the time.
Tested in spring-boot 2.7.2

rsobies
  • 293
  • 4
  • 16