I'm not even sure I'm knocking on the right door so bear with me on this. I have a service, that's being called by another service (upstream) and calls a third service (downstream). My service provides auth mech X for the upstream, but should call downstream with oauth2 due to a change on that service.
Let's say I have an HttpInterface like below:
@HttpExchange("/api")
public interface DownstreamClient {
@GetExchange("/quotes/{id}")
QouteResponse getQuoteById(
@PathVariable("id") String id,
@RequestParam("type") String type);
}
For this I have properties defined in my app-{env}.yaml profile as:
downstream:
location: "https://downstream.domain.com"
oauth2:
registration-id: "..."
client-id: "..."
client-name: "..."
client-secret:
key: "..."
value: "..."
redirectUri: "..."
authorizationUri: "..."
tokenUri: "..."
scope: "..."
From this, I attempted to build a bean of type DownstreamClient that's backed by a WebClient that's able to authenticate the service itself with downstream while not touching the SecurityContextHolder that contains the Authentication for "upstream" and is required for that purpose. Naively, I tried to use spring-security-oauth2-client using ServerOAuth2AuthorizedClientExchangeFilterFunction as the description seems relevant to my usecase. I started from there, basically doing:
@Configuration
public class DownstreamClientConfiguration {
@Bean
DownstreamClient downstreamClient(WebClient downstreamClient) {
return HttpServiceProxyFactory
.builderFor(WebClientAdapter.create(downstreamClient))
.build()
.createClient(DownstreamClient.class);
}
@Bean
WebClient downstreamClient(DownstreamProperties downstreamProps) {
ClientRegistration clientRegistration = ClientRegistration.withRegId....;
ReactiveClientRegistrationRepository clientRegistrationRepository =
new InMemoryReactiveClientRegistrationRepository(clientRegistration);
ReactiveOAuth2AuthorizedClientService authorizedClientService =
new ReactiveOAuth2AuthorizedClientService(clientRegistrationRepository);
ReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new ReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Filter =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Filter.setDefaultClientRegistrationId(downstreamProps.oauth2().registrationId());
return WebClient.builder().filter(oauth2Filter).baseUrl(downstreamProps.location()).build();
}
}
Well something along these lines I guess seemed to be sufficient. Then, I learnt that every single time I received "unauthorized_client" back from the authorization server so I digged and digged and I found couple crucial looking piece of information that was not so clear from the spring.io documentation of this topic. Namely that I need to call:
.attribute(oauth2AuthorizedClient(authorizedClient))
where authorizedClient based on my current very limited knowledge on this topic comes from me adding a controller argument:
@RegisteredOAuth2AuthorizedClient(registrationId = "...")
OAuth2AuthorizedClient authorizedClient
As all this flow happens in the service layer (@Service bean) and I'm trying to authenticate "myself" (the service itself running) with downstream while not even looking at my own service's security implementation, and this annotation doesn't seem to havean effect in an @Service annotated bean method:
@Service
public class MyNaiveServiceExample {
@Autowired private DownstreamClient downstreamClient;
public Optional<QouteDTO> getQouteById(
@RegisteredOAuth2AuthorizedClient("...")
OAuth2AuthorizedClient authorizedClient,
String id) {
QouteReponse response = downstreamClient(authorizedClient, id, "quoteId");
return Optional.of(QuoteDTO.from(response));
}
}
In a nutshell, that would be my usecase. This however obviously doesn't work, so I checked OAuth2AuthorizedClientService or OAuth2AuthorizedClientProvider (namely ClientCredentialsOAuth2AuthorizedClientProvider as I have a client-secret).
OAuth2AuthorizedClientService also doesn't seem to be the way to go because:
T loadAuthorizedClient(String clientRegistrationId, String principalName) Returns the OAuth2AuthorizedClient associated to the provided client registration identifier and End-User's Principal name or null if not available.
ClientCredentialsOAuth2AuthorizedClientProvider seems more like what I need, but I'll be honest I have no idea what the expected OAuth2AuthorizationContext might be that is required for getting an instance of OAuth2AuthorizedClient.
I don't need that... do not even look at the authenticated principal, do it for ourselves! Still, there seems to be an issue on how on earth am I going to set that attribute on the WebClient behind the proxy. I guess, I could use something like:
@GetExchange("/quotes/{id}")
QouteResponse getQuoteById(
@PathVariable("id") String id,
@RequestParam("type") String type,
@RequestAttribute("org.springframework.security.oauth2.client.OAuth2AuthorizedClient") WebClient authorizedClient);
Looks horrendous but that's the name of the attribute that the exchangefilterfunction uses for resolving the client. What I'm trying to do..., can it be done using spring-security-oauth2-client?
@Addition to RestClient based solution:
@Configuration
public class DownstreamClientConfiguration {
@Bean
ClientRegistration clientRegistration() {
return ClientRegistration.<omitted>.build();
}
@Bean
ClientRegistrationRepository clientRegistrationRepository(ClientRegistration clientRegistration) {
return new InMemoryClientRegistrationRepository(clientRegistration);
}
@Bean
OAuth2AuthorizedClientService authorizedClientService(ClientRegistrationRepository clientRegistrationRepository) {
return new InMemoryOAuth2AuthorizedCClientService(clientRegistrationRepository);
}
@Bean
OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) {
return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
}
@Bean
OAuth2AuthorizedClientProvider authorizedClientProvider() {
return OAuth2AuthorizedClientProviderBuilder.builder().clientCredentials().build();
}
@Bean
OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository, OAuth2AuthorizedClientProvider authorizedClientProvider) {
var authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
<RestClient omitted>
}
RestClient is indeed easier to debug. The client sends a request to the (might be wrong on this, I logged out already) authorizationUri containing Authorization: Basic base64(clientId:clientSecret) that seems to be in place. Then I get back az unauthorized_client error, that was the point I gave up for the day.
My issue understanding this version is around OAuth2AuthorizedClientRepository. This interface has two implementations:
Neither seems to be good for service-to-service authentication, I wonder why there's no InMemory version of it. The first associates the oauth2 client with the authenticated user whoever called my service (wrong, it should be the entity running my service - aka my service itself not a user who'll have 0 access to my downstream service). HttpSession on the other hand seems completely off as my service is configured with sessionCreationPolicy.STATELESS, there are no sessions here as far as I'm concerned (fair enough session returns null).
I'm sure it a lot more simple when spring does it's magic and one can just autowire stuff and things magically work, I just want to understand how to actually do it before I abstract things away.
First, as you seem to be working with a servlet, RestClient
(synchronized) is probably more adapted than WebClient
(reactive). If you want to stick with WebClient
, replace the OAuth2ClientHttpRequestInterceptor
in the following with the ServerOAuth2AuthorizedClientExchangeFilterFunction
(in a reactive app) or ServletOAuth2AuthorizedClientExchangeFilterFunction
(in a servlet).
Second, it would be easier if you were using spring-boot-starter-security-oauth2-client
and Spring Boot properties:
spring:
security:
oauth2:
client:
provider:
sso:
issuer-uri: ${issuer}
registration:
downstream-api-registration:
provider: sso
authorization-grant-type: client_credentials
client-id: chouette-api
client-secret: change-me
scope: openid
This would register the authorized clients repo and manager you need.
Last, to avoid confusion between the RestClient
instance and @HttpExchange
implementation, I rename the later to DownstreamApi
:
@HttpExchange("/api")
public interface DownstreamApi {
@GetExchange("/quotes/{id}")
QouteResponse getQuoteById(@PathVariable("id") String id, @RequestParam("type") String type);
}
Provided that you have such a property:
downstream-api-base-uri: http://localhost:8180
You can define a RestClient
bean as follows:
@Bean
RestClient downstreamRestClient(
@Value("downstream-api-base-uri") URI downstreamApiBaseUri,
OAuth2AuthorizedClientManager authorizedClientManager,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
return RestClient.builder().baseUrl(downstreamApiBaseUri)
.requestInterceptor(registrationClientHttpRequestInterceptor(authorizedClientManager,
authorizedClientRepository, "downstream-api-registration"))
.build();
}
ClientHttpRequestInterceptor registrationClientHttpRequestInterceptor(
OAuth2AuthorizedClientManager authorizedClientManager,
OAuth2AuthorizedClientRepository authorizedClientRepository,
String registrationId) {
final var interceptor = new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);
interceptor.setClientRegistrationIdResolver((HttpRequest request) -> registrationId);
interceptor.setAuthorizationFailureHandler(
OAuth2ClientHttpRequestInterceptor.authorizationFailureHandler(authorizedClientRepository));
return interceptor;
}
You can have Spring generate an implementation for your @HttpExchange
interface as follows:
@Bean
DownstreamApi downstreamApi(RestClient downstreamRestClient) {
return HttpServiceProxyFactory.builderFor(RestClientAdapter.create(downstreamRestClient)).build()
.createClient(DownstreamApi.class);
}
At voilĂ ! you can auto-wire this downstreamApi
in any @Component
you like.
spring-addons-starter-rest
I maintain a Spring Boot Strater to ease RestClient
and WebClient
configuration. With it, you can configure request authorizations with OAuth2 client credentials flow as follows (keeping the "official" boot properties for the client registration
):
com:
c4-soft:
springaddons:
rest:
client:
downstream-rest-client:
base-url: http://localhost:8180
authorization:
oauth2:
oauth2-registration-id: downstream-api-registration
@Bean
DownstreamApi downstreamApi(RestClient downstreamRestClient) throws Exception {
return new RestClientHttpExchangeProxyFactoryBean<>(DownstreamApi.class, downstreamRestClient).getObject();
}
Configuration options using application properties include:
Basic
, static API key, or forwarding a token in the security context of a resource server)HTTP_PROXY
and NO_PROXY
environment variables)