Tag Archives: spring-boot

Spring Boot app client of an AUTH0 protected service (JWT)

While yesterday I was very happy to share the good developper experience I got recently with Auth0, today I am feeling frustrated by something I was expecting to be easy and which finally isn’t.

I am sharing my experience here, with the hope it can help someone else to have the same headache….

The context is very simple:

A Spring Boot application (latest 2.4.x) needs to connect to a REST service protected with a JWT Token using Auth0

I was expecting to implement in five minutes… It was more 5 hours.. :-/

While I like the large ecosystem which is proposed by Spring and the reference documentations which are (in general) of good quality it’s often very hard to figure out for a given use case what is the correct way to implement it due to variety of resources and the lack of up-to-date tutorial.

Googling about it is a big challenge because the terms are generic and the Spring stack evolved especially with Spring Security 5 and it’s “native” support for Oauth.

In general “How to” articles which are very useful are more on @baeldung website than on Spring docs :-/

But then, to come back to my need, I discovered that @auth0 requirement to pass an “audience” to get the JWT token isn’t considered as very standard ( https://github.com/spring-projects/spring-security/issues/7379 ).

Hopefully we can find some resources like https://github.com/spring-projects/spring-security/issues/6569 to explain how to customise the behaviour but it’s based on an old version of Spring Security and a part of this is deprecated. (I also discovered on SO that the well known RestTemplate is deprecated and I should move to the WebClient – https://stackoverflow.com/questions/58982286/spring-security-5-replacement-for-oauth2resttemplate )

Maybe I missed a better solution to do it but what I found is that to add this audience in the request I have to create a Converter<OAuth2ClientCredentialsGrantRequest, RequestEntity<?>>.

But I cannot reuse OAuth2ClientCredentialsGrantRequestEntityConverter
which is not extensible.

Thus I ended with 100 lines of copy/paste (BEURK) to just add one useful line: formParameters.add("audience", this.audience);

import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequestEntityConverter;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.util.Collections;
/**
* <p>This class is a copy of {@link OAuth2ClientCredentialsGrantRequestEntityConverter} and
* {@link org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationGrantRequestEntityUtils} just
* to add the {code audience} to the Authorization request sent to Auth0.</p>
* <p>Sadly these classes aren't extensible and thus the copy/paste.</p>
*
* @see <a href="https://github.com/spring-projects/spring-security/issues/6569">issue 6569</a>
* @see <a href="https://github.com/spring-projects/spring-security/issues/7379">issue 7379</a>
* @since 1.1.0
*/
public final class Auth0ClientCredentialsGrantRequestEntityConverter
implements Converter<OAuth2ClientCredentialsGrantRequest, RequestEntity<?>> {
private static final HttpHeaders DEFAULT_TOKEN_REQUEST_HEADERS = getDefaultTokenRequestHeaders();
private final String audience;
/**
* @param audience The audience to pass to Auth0
*/
public Auth0ClientCredentialsGrantRequestEntityConverter(String audience) {
this.audience = audience;
}
/**
* Returns the {@link RequestEntity} used for the Access Token Request.
*
* @param clientCredentialsGrantRequest the client credentials grant request
* @return the {@link RequestEntity} used for the Access Token Request
*/
@Override
public RequestEntity<?> convert(OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) {
ClientRegistration clientRegistration = clientCredentialsGrantRequest.getClientRegistration();
HttpHeaders headers = getTokenRequestHeaders(clientRegistration);
MultiValueMap<String, String> formParameters = this.buildFormParameters(clientCredentialsGrantRequest);
URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getTokenUri()).build()
.toUri();
return new RequestEntity<>(formParameters, headers, HttpMethod.POST, uri);
}
/**
* Returns a {@link MultiValueMap} of the form parameters used for the Access Token
* Request body.
*
* @param clientCredentialsGrantRequest the client credentials grant request
* @return a {@link MultiValueMap} of the form parameters used for the Access Token
* Request body
*/
private MultiValueMap<String, String> buildFormParameters(
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) {
ClientRegistration clientRegistration = clientCredentialsGrantRequest.getClientRegistration();
MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
formParameters
.add(OAuth2ParameterNames.GRANT_TYPE, clientCredentialsGrantRequest.getGrantType().getValue());
if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) {
formParameters.add(OAuth2ParameterNames.SCOPE,
StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), " "));
}
if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
formParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
}
formParameters.add("audience", this.audience);
return formParameters;
}
private static HttpHeaders getTokenRequestHeaders(ClientRegistration clientRegistration) {
HttpHeaders headers = new HttpHeaders();
headers.addAll(DEFAULT_TOKEN_REQUEST_HEADERS);
if (ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
}
return headers;
}
private static HttpHeaders getDefaultTokenRequestHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
final MediaType contentType =
MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
headers.setContentType(contentType);
return headers;
}
}

And a big method to configure correctly the WebClient

/**
* <p>Create a {@link WebClient} using OAuth2 (Auth0) authentication (MTM)</p>
*
* @param clientRegistrationRepository The repository of client registrations
* @param authorizedClientRepository The repository of authorized clients
* @param clientRegistrationId The Client Registration Id to use to get the authentication info (clientId, clientSecret, scope…)
* @param url The base URL of REST Service this client is interacting with
* @param audience The audience to pass to the authentication request sent to Auth0
* @return a {@link WebClient} instance preconfigured to access to the remote service
* @since 1.1.0
*/
@Nonnull
public static WebClient createWebClient(@Nonnull ClientRegistrationRepository clientRegistrationRepository,
@Nonnull OAuth2AuthorizedClientRepository authorizedClientRepository,
@Nonnull String clientRegistrationId,
@Nonnull String url,
@Nonnull String audience) {
Converter<OAuth2ClientCredentialsGrantRequest, RequestEntity<?>> customRequestEntityConverter =
new Auth0ClientCredentialsGrantRequestEntityConverter(audience);
DefaultClientCredentialsTokenResponseClient clientCredentialsTokenResponseClient =
new DefaultClientCredentialsTokenResponseClient();
clientCredentialsTokenResponseClient.setRequestEntityConverter(customRequestEntityConverter);
ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialsOAuth2AuthorizedClientProvider =
new ClientCredentialsOAuth2AuthorizedClientProvider();
clientCredentialsOAuth2AuthorizedClientProvider
.setAccessTokenResponseClient(clientCredentialsTokenResponseClient);
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(clientCredentialsOAuth2AuthorizedClientProvider);
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId(clientRegistrationId);
return WebClient.builder()
.baseUrl(url)
.apply(oauth2Client.oauth2Configuration())
.build();
}
view raw Auth0Utils.java hosted with ❤ by GitHub

I probably missed a lost of things and it could be done differently but it works. It’s just very frustrating to not have to concrete tutorials to handle something which is supposed to be “basic”. I could probably blame Auth0 to need this extra audience field but sooo much code for 1 line to add, I am very sad..