I was in the same situation as you and found a solution. First off, to see it in action, I have created a repository with a showcase implementation of everything that is explained below.
is there an 'cleaner' way to replace the WebClient bean I am using, by one that will call my fake remote service without trying to get a token first ?
I would not replace the WebClient
bean in your test, but rather replace the ReactiveOAuth2AuthorizedClientManager
bean with a mock.
For this to work you have to slightly modify your MyRemoteServiceClientOauth2Config
. Instead of using the now deprecated approach with an UnAuthenticatedServerOAuth2AuthorizedClientRepository
you configure it this way (this is also more in line with the documented configuration on the Servlet-Stack):
@Configuration
public class MyRemoteServiceClientOauth2Config {
@Bean
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager reactiveOAuth2AuthorizedClientManager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2ClientCredentialsFilter =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(reactiveOAuth2AuthorizedClientManager);
oauth2ClientCredentialsFilter.setDefaultClientRegistrationId("myRemoteService");
return WebClient.builder()
.filter(oauth2ClientCredentialsFilter)
.build();
}
@Bean
public ReactiveOAuth2AuthorizedClientManager reactiveOAuth2AuthorizedClientManager(ReactiveClientRegistrationRepository clientRegistrations,
ReactiveOAuth2AuthorizedClientService authorizedClients) {
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, authorizedClients);
authorizedClientManager.setAuthorizedClientProvider(
new ClientCredentialsReactiveOAuth2AuthorizedClientProvider());
return authorizedClientManager;
}
}
Then you can create a mock of ReactiveOAuth2AuthorizedClientManager
that always returns a Mono
of an OAuth2AuthorizedClient
like this:
@TestComponent
@Primary
public class AlwaysAuthorizedOAuth2AuthorizedClientManager implements ReactiveOAuth2AuthorizedClientManager {
@Value("${spring.security.oauth2.client.registration.myRemoteService.client-id}")
String clientId;
@Value("${spring.security.oauth2.client.registration.myRemoteService.client-secret}")
String clientSecret;
@Value("${spring.security.oauth2.client.provider.some-keycloak.token-uri}")
String tokenUri;
/**
* {@inheritDoc}
*
* @return
*/
@Override
public Mono<OAuth2AuthorizedClient> authorize(final OAuth2AuthorizeRequest authorizeRequest) {
return Mono.just(
new OAuth2AuthorizedClient(
ClientRegistration
.withRegistrationId("myRemoteService")
.clientId(clientId)
.clientSecret(clientSecret)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.tokenUri(tokenUri)
.build(),
"some-keycloak",
new OAuth2AccessToken(TokenType.BEARER,
"c29tZS10b2tlbg==",
Instant.now().minus(Duration.ofMinutes(1)),
Instant.now().plus(Duration.ofMinutes(4)))));
}
}
And finally @Import
that in your test:
@SpringBootTest
@Import(AlwaysAuthorizedOAuth2AuthorizedClientManager.class)
class YourIntegrationTestClass {
// here is your test code
}
The corresponding src/test/resources/application.yml
looks like this:
spring:
security:
oauth2:
client:
registration:
myRemoteService:
authorization-grant-type: client_credentials
client-id: test-client
client-secret: 6b30087f-65e2-4d89-a69e-08cb3c9f34d2 # bogus
provider: some-keycloak
provider:
some-keycloak:
token-uri: https://some.bogus/token/uri
Alternative
You could also just use the same mockserver
you are already using to mock your REST-Resource, to also mock the Authorization server and respond to the token request. For this to work, you would configure the mockserver
as the token-uri
in the src/test/resources/application.yml
or whatever you are using to provide properties to your test respectively.
Notes
Injecting the WebClient
directly
The recommended way of providing a WebClient
in your beans is by injecting WebClient.Builder
, which gets preconfigured by Spring Boot. This also guarantees, that the WebClient
in your test is configured exactly the same as in production. You can declare WebClientCustomizer
beans to configure this builder further. This is the way it is implemented in my showcase repository mentioned above.
Bean overriding/prioritizing with @Primary
on a @Bean
inside a @Configuration
or @TestConfiguration
I have tried that too and found that it does not always work the way one would expect, probably because of the order in which Spring loads and instantiates the bean definitions. For instance, the ReactiveOAuth2AuthorizedClientManager
mock is only used if the @TestConfiguration
is a static nested
class inside the test class but not if it is @Import
ed. Having the static nested @TestConfiguration
on an interface and implement that with the test class also does not work. So, to avoid putting that static nested
class on every integration test I need it in, I rather opt for the @TestComponent
approach presented here.
Other OAuth 2.0 Grant Types
I only tested my approach for the Client Credentials
Grant Type, but I think it could also be adapted or expanded for other Grant Types as well.