I have a small Spring Boot 2.2 batch that writes to an OAuth2 REST API.
I have been able to configure the WebClient
following https://medium.com/@asce4s/oauth2-with-spring-webclient-761d16f89cdd and it works as expected.
@Configuration
public class MyRemoteServiceClientOauth2Config {
@Bean("myRemoteService")
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(
clientRegistrations,
new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
oauth.setDefaultClientRegistrationId("myRemoteService");
return WebClient.builder()
.filter(oauth)
.build();
}
}
However, now I would like to write an integration test for my batch, and I would like to avoid using the "real" authorization server to get a token : I don't want my test to fail if an external server is down. I want my test to be "autonomous".
The remote service I am calling is replaced by a mockserver
fake one during my tests.
What is the best practice in that case ?
@Profile("!test")
and run my tests with @ActiveProfiles("test")
. I also import a test specific config in my test : @Configuration
@Profile("test")
public class BatchTestConfiguration {
@Bean("myRemoteService")
public WebClient webClientForTest() {
return WebClient.create();
}
}
But I feel having to add @Profile("!test")
on my production config is not great..
@Primary
on my webClientForTest bean, but it doesn't work : the production bean still gets enabled and I get an exception : No qualifying bean of type 'org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
which is the parameter type the production bean needs
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
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.
WebClient
directlyThe 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.
@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.
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.