78 lines
3.5 KiB
Markdown
78 lines
3.5 KiB
Markdown
# 3. `RestClient` interceptors
|
|
|
|
*Prev: [2. Three ways to get a token](02-three-ways-to-get-a-token.md) · Next: [4. What a resource server does not validate](04-what-is-not-validated.md)*
|
|
|
|
`OAuth2ClientHttpRequestInterceptor`, in
|
|
`org.springframework.security.oauth2.client.web.client`, is the framework's answer for a
|
|
`RestClient` that needs a token. Its whole public surface:
|
|
|
|
```java
|
|
public OAuth2ClientHttpRequestInterceptor(OAuth2AuthorizedClientManager manager);
|
|
public void setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler handler);
|
|
public void setClientRegistrationIdResolver(ClientRegistrationIdResolver resolver);
|
|
public void setPrincipalResolver(PrincipalResolver resolver);
|
|
```
|
|
|
|
Wiring it takes one line, and choosing the registration per call takes one more:
|
|
|
|
```java
|
|
RestClient client = builder
|
|
.baseUrl("http://127.0.0.1:8082")
|
|
.requestInterceptor(new OAuth2ClientHttpRequestInterceptor(authorizedClientManager))
|
|
.build();
|
|
|
|
client.get().uri("/orders")
|
|
.attributes(clientRegistrationId("edge-service")) // static import
|
|
.retrieve().body(Map.class);
|
|
```
|
|
|
|
`clientRegistrationId(..)` is a static method on `RequestAttributeClientRegistrationIdResolver`.
|
|
Without it, the default resolver finds nothing and the request goes out unauthenticated.
|
|
|
|
## Which `OAuth2AuthorizedClientManager`
|
|
|
|
This is the choice that decides whether the thing works off a request thread.
|
|
|
|
| Manager | Storage | Needs a request? |
|
|
|---|---|---|
|
|
| `DefaultOAuth2AuthorizedClientManager` | `OAuth2AuthorizedClientRepository` (session) | **Yes** |
|
|
| `AuthorizedClientServiceOAuth2AuthorizedClientManager` | `OAuth2AuthorizedClientService` | No |
|
|
|
|
For service-to-service calls there is no end user whose authorization is being stored per
|
|
session, so the second one is right — and it is the one that keeps working from a scheduled
|
|
task, a message listener or an `@Async` method.
|
|
|
|
Getting this wrong produces `ClientAuthorizationRequiredException` or a silent `null` in a
|
|
context that has no `HttpServletRequest`, which reads like an OAuth problem and is a bean
|
|
problem.
|
|
|
|
## What it caches, and what it does not
|
|
|
|
The manager stores the authorized client (access token and, if issued, refresh token) in the
|
|
`OAuth2AuthorizedClientService` and reuses it until it is within the clock skew of expiry.
|
|
So a `client_credentials` registration does **not** hit the token endpoint per request. What it
|
|
does do is re-request on expiry, synchronously, inside whichever call happens to be first —
|
|
worth knowing when a latency percentile spikes on a period that matches your token lifetime.
|
|
|
|
## The hand-rolled relay, and why it is still reasonable
|
|
|
|
The relay interceptor in
|
|
[`DownstreamClients`](../src/main/java/com/ankurm/s2s/edge/DownstreamClients.java) does not use
|
|
any of the above:
|
|
|
|
```java
|
|
var authentication = SecurityContextHolder.getContext().getAuthentication();
|
|
if (authentication instanceof JwtAuthenticationToken token) {
|
|
request.getHeaders().setBearerAuth(token.getToken().getTokenValue());
|
|
}
|
|
```
|
|
|
|
That is not a worse version of `OAuth2ClientHttpRequestInterceptor`; it is a different thing.
|
|
The interceptor **obtains** a token under a client registration. This **forwards** the token
|
|
already in hand. There is no client registration for "the caller's token", and there should not
|
|
be. Just do not confuse the two: the hand-rolled one carries the `ThreadLocal` dependency from
|
|
chapter 2, and the framework one does not.
|
|
|
|
---
|
|
*Prev: [2. Three ways to get a token](02-three-ways-to-get-a-token.md) · Next: [4. What a resource server does not validate](04-what-is-not-validated.md)*
|