1
0
Files
spring-security-demo/service-to-service/docs/02-three-ways-to-get-a-token.md

101 lines
4.1 KiB
Markdown

# 2. Three ways to get a token for the next hop
*Prev: [1. The four processes](01-the-four-processes.md) · Next: [3. RestClient interceptors](03-restclient-interceptors.md)*
The edge service has a valid token in its hands and needs to call `downstream`. There are three
answers, and [`docs/output/02-five-strategies.txt`](output/02-five-strategies.txt) runs all of
them against the same request so the difference is a diff.
| Endpoint | `sub` downstream sees | `scope` downstream sees |
|---|---|---|
| `/edge/naive` | — (401) | — |
| `/edge/relay` | `alice` | `[orders.write, orders.read]` |
| `/edge/client-credentials` | `edge-service` | `[orders.read]` |
| `/edge/exchange` | `alice` | `[orders.read]` |
| `/edge/relay-async` | — (401) | — |
## Relay
Forward the token you were given, unchanged. Ten lines, no dependency on the OAuth2 client
machinery:
```java
builder.requestInterceptor((request, body, execution) -> {
var authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof JwtAuthenticationToken token) {
request.getHeaders().setBearerAuth(token.getToken().getTokenValue());
}
return execution.execute(request, body);
})
```
Downstream sees `alice`, which is what you want in an audit log. It also sees
`orders.write`, which downstream had no business receiving: the token was minted for the
request the user made at the edge, and every service it is relayed to gets the union of
everything the user consented to. One compromised service holds a credential good against all
of them for the rest of its lifetime.
Relay is right for a hop inside one trust boundary. It is wrong the moment the next hop is
operated by someone who should not hold the user's full token.
## Client credentials
The service authenticates as itself:
```yaml
spring.security.oauth2.client.registration.edge-service:
authorization-grant-type: client_credentials
scope: orders.read
```
Downstream now sees `sub: edge-service` and exactly `orders.read`. The scope problem is solved.
The user is gone — `downstream`'s log says a service called it, and answering "who ordered
this?" requires correlating two logs by a request id you hope somebody propagated.
## Token exchange (RFC 8693)
Trade the user's token for one scoped to the next hop, keeping the user:
```yaml
authorization-grant-type: urn:ietf:params:oauth:grant-type:token-exchange
scope: orders.read
```
`sub: alice`, `scope: [orders.read]`. Both properties, from one grant type that has existed
since 2020 and that almost nobody reaches for.
The catch is configuration, not concept. `OAuth2AuthorizedClientProviderBuilder`'s defaults do
**not** include token exchange, so you have to add the provider yourself:
```java
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.refreshToken()
.provider(new TokenExchangeOAuth2AuthorizedClientProvider())
.build();
```
Leave it out and the manager returns `null` rather than raising anything, and the interceptor
sends the request with no `Authorization` header at all. A 401 that looks like a downstream
problem and is a wiring problem.
## The trap: relay from another thread
`/edge/relay-async` does exactly what `/edge/relay` does, on a virtual thread from an executor.
It returns 401.
`SecurityContextHolder` is a `ThreadLocal`. The interceptor reads it; the task runs on a thread
that never had a `SecurityContext` written to it; the `instanceof` fails; the header is never
set. Nothing throws. This is the same mechanism as
[Spring Security Context Propagation](https://ankurm.com/spring-security-context-propagation-complete-guide/),
and the cure is the same: `DelegatingSecurityContextExecutorService`, or
`ContextPropagatingTaskDecorator`, or capture the token value on the request thread and pass it
as a parameter.
Any relay built on `SecurityContextHolder` inherits every one of those failure modes. A relay
built on an explicit token parameter inherits none of them, at the cost of a parameter on every
method between the controller and the client.
---
*Prev: [1. The four processes](01-the-four-processes.md) · Next: [3. RestClient interceptors](03-restclient-interceptors.md)*