Summary
I am writing a reactive Spring application that processes Google Pub/Sub messages with Spring Cloud Stream. For each message Auth0's management API should be called with a OAuth 2.0 client credentials token. However, the WebClient won't perform an authorization.
Actual Behavior
I don't have spring-boot-starter-security in my project. This is a "standalone" application that processes messages and calls for each message Auth0's management API.
I have created a WebClient that should automatically use a OAuth 2.0 client credentials token. Unfortunately, the WebClient never performs an authorization. It just performs the call to the call to the management API without getting a token beforehand.
This is the error message:
2020-02-13 12:25:46.336 ERROR 5898 --- [ctor-http-nio-3] i.v.outputservice.auth0.Auth0Service : illegal response! status code: 401 UNAUTHORIZED, body: {"statusCode":401,"error":"Unauthorized","message":"Missing authentication"}
Expected Behavior
WebClient should get a client credentials
Configuration
My Spring dependencies in build.gradle:
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
// ...
implementation 'org.springframework.security:spring-security-oauth2-client'
This is the @Configuration class:
@Configuration
@RequiredArgsConstructor
class Auth0Configuration {
private static final String AUTH_0_CLIENT_REGISTRATION_ID = "auth0";
@Value("${auth0.api.base-url}")
private final String auth0ApiBaseUrl;
@Value("${auth0.token-uri}")
private final String auth0TokenUri;
@Value("${auth0.client-id}")
private final String auth0ClientId;
@Value("${auth0.client-secret}")
private final String auth0ClientSecret;
@Bean
WebClient auth0WebClient() {
return WebClient.builder()
.filter(createServerOAuth2AuthorizedClientExchangeFilterFunction(createClientRegistration()))
.baseUrl(auth0ApiBaseUrl)
.clientConnector(new ReactorClientHttpConnector(HttpClient.create().wiretap(true)))
.build();
}
private static ServerOAuth2AuthorizedClientExchangeFilterFunction createServerOAuth2AuthorizedClientExchangeFilterFunction(ClientRegistration clientRegistration) {
ServerOAuth2AuthorizedClientExchangeFilterFunction serverOAuth2AuthorizedClientExchangeFilterFunction =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(
new DefaultReactiveOAuth2AuthorizedClientManager(
new InMemoryReactiveClientRegistrationRepository(clientRegistration),
new UnAuthenticatedServerOAuth2AuthorizedClientRepository()
));
serverOAuth2AuthorizedClientExchangeFilterFunction.setDefaultClientRegistrationId(AUTH_0_CLIENT_REGISTRATION_ID);
return serverOAuth2AuthorizedClientExchangeFilterFunction;
}
private ClientRegistration createClientRegistration() {
return ClientRegistration.withRegistrationId(AUTH_0_CLIENT_REGISTRATION_ID)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.tokenUri(auth0TokenUri)
.clientId(auth0ClientId)
.clientSecret(auth0ClientSecret)
.build();
}
}
Version
Spring versions:
id 'org.springframework.boot' version '2.2.4.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
Java 13
Sample
@Qualifier("auth0WebClient")
private final WebClient webClient;
// see https://auth0.com/docs/api/management/v2#!/Users/get_users_by_id
public Mono<User> getUser(String id) {
return webClient.get()
.uri(uriBuilder ->
uriBuilder.path("/api/v2/users/{id}")
.queryParam("fields", "email,email_verified")
.queryParam("include_fields", true)
.build(id))
.exchange()
.handle((BiConsumer<ClientResponse, SynchronousSink<ClientResponse>>) (clientResponse, synchronousSink) -> {
HttpStatus httpStatus = clientResponse.statusCode();
if (httpStatus.is2xxSuccessful()) {
synchronousSink.next(clientResponse);
} else {
synchronousSink.error(new IllegalResponseException(clientResponse));
}
})
.doOnError(IllegalResponseException.class, e -> {
ClientResponse clientResponse = e.clientResponse;
clientResponse.bodyToMono(String.class).subscribe(body ->
log.error("illegal response! status code: {}, body: {}", clientResponse.statusCode(), body));
})
.doOnNext(clientResponse -> log.debug("status code: {}", clientResponse.statusCode()))
.flatMap((Function<ClientResponse, Mono<User>>) clientResponse -> clientResponse.bodyToMono(User.class));
}
Comment From: ghost
The application is working now with this configuration:
@Configuration
@RequiredArgsConstructor
@Slf4j
class Auth0Configuration {
// some properties
@Bean
WebClient auth0WebClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
var exchangeFilterFunction = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
exchangeFilterFunction.setDefaultClientRegistrationId(AUTH_0_CLIENT_REGISTRATION_ID);
return WebClient.builder()
.baseUrl(auth0ApiBaseUrl)
.filter(exchangeFilterFunction)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
@Bean
public ReactiveClientRegistrationRepository reactiveClientRegistrationRepository() {
return new InMemoryReactiveClientRegistrationRepository(
ClientRegistration.withRegistrationId(AUTH_0_CLIENT_REGISTRATION_ID)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.tokenUri(auth0TokenUri)
.clientAuthenticationMethod(ClientAuthenticationMethod.POST)
.clientId(auth0ClientId)
.clientSecret(auth0ClientSecret)
.scope()
.build()
);
}
@Bean
public ReactiveOAuth2AuthorizedClientManager reactiveOAuth2AuthorizedClientManager(ReactiveClientRegistrationRepository clientRegistrationRepository) {
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultReactiveOAuth2AuthorizedClientManager(clientRegistrationRepository, new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
authorizedClientManager.setAuthorizedClientProvider(ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials(clientCredentialsGrantBuilder -> {
WebClientReactiveClientCredentialsTokenResponseClient accessTokenResponseClient = new WebClientReactiveClientCredentialsTokenResponseClient();
accessTokenResponseClient.setWebClient(WebClient.builder()
.filter((request, next) -> {
FormInserter<String> body = (FormInserter<String>) request.body();
body.with("audience", auth0Audience);
return next.exchange(request);
})
.build());
clientCredentialsGrantBuilder.accessTokenResponseClient(accessTokenResponseClient);
}
)
.build()
);
return authorizedClientManager;
}
}