I'm trying to implement an oauth2 secured Spring Boot API with Spring Security 5. I want the API to be an oauth2 resource server and to be able to access external oauth2 resource servers using a WebClient, with client credentials grant.
I can configure the API to be an oauth2 resource server or an oauth2 client, but not both at the same time.
Below is the minimal setup for configuring the API to be a resource server with Spring security 5. I'm using Opaque Tokens so server is configured for that.
application.properties
spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=http://localhost:8086/auth/oauth/check_token
spring.security.oauth2.resourceserver.opaquetoken.client-id=test-api
spring.security.oauth2.resourceserver.opaquetoken.client-secret=e61aa5d6-074d-4216-b15f-1bf3fc71b833
WebSecurity Configuration class
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken);
}
}
With this setup I can hit this api endpoints with a valid token to access its protected resources. So resource server configuration works fine on its own.
Below is the minimal Spring security 5 setup configured to use a WebClient to access external protected resources using client credentials grant.
application.properties
spring.security.oauth2.client.provider.my-oauth-provider.token-uri=http://127.0.0.1:8086/auth/oauth/token
spring.security.oauth2.client.registration.test-api.client-id=test-store
spring.security.oauth2.client.registration.test-api.client-secret=password
spring.security.oauth2.client.registration.test-api.provider=my-oauth-provider
spring.security.oauth2.client.registration.test-api.scope=read,write
spring.security.oauth2.client.registration.test-api.authorization-grant-type=client_credentials
WebSecurity Configuration class
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
@Autowired
ClientRegistrationRepository clientRegistrationRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Client(oauth2 -> oauth2
.clientRegistrationRepository(clientRegistrationRepository)
);
}
}
Oauth2Client Configuration class
@Configuration
public class Oauth2ClientConfig {
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
@Bean
WebClient webClient(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(
oAuth2AuthorizedClientManager);
// default registrationId
oauth2Client.setDefaultClientRegistrationId("test-api");
// set client to use oauth2 by default globally
oauth2Client.setDefaultOAuth2AuthorizedClient(true);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
}
With this setup I can hit an external protected endpoint to access a protected resource using the client credentials grant.
However when I put these two configurations together, it doesn't work when I try to hit my api endpoint which in turn tries to access an external resource using WebClient.
@Autowired
WebClient webClient;
@GetMapping("test")
public String test() {
String message = webClient
.get()
.uri("http://localhost:8084/external/api/endpoint")
.retrieve()
.bodyToMono(String.class)
.block();
return message;
}
Spring security throws a principalName cannot be empty error.
java.lang.IllegalArgumentException: principalName cannot be empty
at org.springframework.util.Assert.hasText(Assert.java:287) ~[spring-core-5.2.8.RELEASE.jar:5.2.8.RELEASE]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ Request to GET http://localhost:8084/ocr/document/test [DefaultWebClient]
Stack trace:
at org.springframework.util.Assert.hasText(Assert.java:287) ~[spring-core-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService.loadAuthorizedClient(InMemoryOAuth2AuthorizedClientService.java:73) ~[spring-security-oauth2-client-5.3.3.RELEASE.jar:5.3.3.RELEASE]
at org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository.loadAuthorizedClient(AuthenticatedPrincipalOAuth2AuthorizedClientRepository.java:73) ~[spring-security-oauth2-client-5.3.3.RELEASE.jar:5.3.3.RELEASE]
at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager.authorize(DefaultOAuth2AuthorizedClientManager.java:144) ~[spring-security-oauth2-client-5.3.3.RELEASE.jar:5.3.3.RELEASE]
at org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.lambda$authorizeClient$24(ServletOAuth2AuthorizedClientExchangeFilterFunction.java:534) ~[spring-security-oauth2-client-5.3.3.RELEASE.jar:5.3.3.RELEASE]
...
Any pointers on solving this issue is highly appreciated. Is there a different way I should configure the WebClient to work when the API is also a resource server?
I have these dependencies in my maven project. Spring security version is 5.3.3.RELEASE pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
...
<!-- Spring Boot Web starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Security Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Oauth2 resource server -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
</dependency>
<!-- Oauth2 client -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<!-- WebClient -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>