# 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)*