Seems that with Auth0, when in a M2M flow, we need to pass the audience
parameter in the authorization request, and the the token will be issued for such audience
curl --request POST \
--url https://domain.eu.auth0.com/oauth/token \
--header 'content-type: application/json' \
--data '{"client_id":"xxxxx","client_secret":"xxxxx","audience":"my-api-audience","grant_type":"client_credentials"}'
otherwise, an error is thrown
403 Forbidden: "{"error":"access_denied","error_description":"No audience parameter was provided, and no default audience has been configured"}"
I try to implement a Client Credentials
flow with Spring Boot using the new Spring Security 5 approach with webflux using WebClient.
Spring doesn't provide a way to add custom parameters to the Auth requests so as by this post
https://github.com/spring-projects/spring-security/issues/6569
I have to implement a custom converter.
Everything seems to be injected fine on startup but the converted is never invoked when accessing the client's endpoint localhost/api/explicit
so I keep stuck with the audience
problem.
WebClientConfig.java
@Configuration
public class WebClientConfig {
@Value("${resource-uri}")
String resourceUri;
@Value("${wallet-audience}")
String audience;
@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
var oauth2 = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.filter(oauth2)
// TRIED BOTH
//.apply(oauth2.oauth2Configuration())
.build();
}
@Bean
OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository) {
Converter<OAuth2ClientCredentialsGrantRequest, RequestEntity<?>> customRequestEntityConverter = new Auth0ClientCredentialsGrantRequestEntityConverter(audience);
// @formatter:off
var authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.refreshToken()
.clientCredentials(clientCredentialsGrantBuilder -> {
var clientCredentialsTokenResponseClient = new DefaultClientCredentialsTokenResponseClient();
clientCredentialsTokenResponseClient.setRequestEntityConverter(customRequestEntityConverter);
})
.build();
// @formatter:on
var authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
Auth0ClientCredentialsGrantRequestEntityConverter.java
thanks to https://www.aheritier.net/spring-boot-app-client-of-an-auth0-protected-service-jwt/
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.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.util.Collections;
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) {
var clientRegistration = clientCredentialsGrantRequest.getClientRegistration();
var headers = getTokenRequestHeaders(clientRegistration);
var formParameters = this.buildFormParameters(clientCredentialsGrantRequest);
var 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) {
var 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) {
var 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() {
var headers = new HttpHeaders();
final var contentType = MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.setContentType(contentType);
return headers;
}
}
Controller.java
@RestController public class PrivateController {
private final WebClient webClient;
public PrivateController(WebClient webClient) {
this.webClient = webClient;
}
@GetMapping("/explicit")
String explicit(Model model, @RegisteredOAuth2AuthorizedClient("wallet") OAuth2AuthorizedClient authorizedClient) {
String body = this.webClient
.get()
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String.class)
.block();
model.addAttribute("body", body);
return "response";
}
}
application.properties
spring.security.oauth2.client.registration.wallet.client-id =
spring.security.oauth2.client.registration.wallet.client-secret =
spring.security.oauth2.client.registration.wallet.scope[] = read:transaction,write:transaction
spring.security.oauth2.client.registration.wallet.authorization-grant-type = client_credentials
spring.security.oauth2.client.provider.wallet.issuer-uri = https://domain.eu.auth0.com/
resource-uri = http://localhost:8081/api/wallet
wallet-audience = https://wallet