Sessions and Tokens — From the Browser to the Mesh
Why you shouldn't pass on the token you received
In one line
The token a service uses when it calls a downstream service has two branches. For work unrelated to a user, it uses a client credentials token received in its own name, and for work on behalf of a user, it hands the received user token to an STS and swaps it for a new token that is "only for this downstream, and records who is calling on behalf" (token exchange). Passing the received token straight through is neither of the two.
Why this was needed
Suppose an order API receives a user token (aud=lab-api) and calls the inventory and payment services for that user. The easiest path is to put the received token straight into Authorization and pass it on. This shortcut has three problems.
First, that token's aud is not downstream. For downstream to accept it, it has to turn off the aud check, and a service that does not look at aud also accepts tokens meant for other services. A token that leaks from one place becomes the key to the whole chain. Second, downstream cannot tell whether the user called directly or a service called on the user's behalf. No actor is left in the audit log. Third, the privileges do not shrink. The privileges of the user token go all the way to the end of the chain.
Conversely, if it uses only the service's own token, downstream knows only "api-svc called" and does not know who the request is for. It cannot make per-user permission decisions. What fills the gap between these two is RFC 8693 OAuth 2.0 Token Exchange.
How it works
client credentials. RFC 6749 §4.4 says to use this grant when a client accesses resources under its own control, and insists that only a confidential client can use it. It is recommended not to put a refresh token in the response (§4.4.3) — because a client holding a secret can just get a new one whenever it likes. When you receive it as api-svc from this lab's Keycloak, a service account named service-account-api-svc becomes the subject instead of a user.
Token exchange. You send the request to the token endpoint like this.
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
subject_token=<사용자 토큰> subject_token_type=...:token-type:access_token (필수)
actor_token=<서비스 토큰> actor_token_type=...:token-type:access_token (선택)
audience=orders-api (선택)
The RFC distinguishes two meanings. Impersonation is A becoming B so that A is indistinguishable from B, and delegation is A acting for B while keeping its own identity. For delegation, you leave the current actor in the new token with the act claim. A nested act is the earlier actors, and the RFC says to use only the top-level claims and the outermost act for access control and to treat the rest as information only. issued_token_type is required in the response. If the subject_token is invalid it returns invalid_request, and if it cannot issue for the requested audience it returns invalid_target.
The responsibility is split in two. The STS verifies the subject_token down to the signature, iss, exp, and aud, checks the audience against an allowlist, narrows the privileges, and gives a short lifetime. An STS that skips verification becomes a machine that launders a forged token with its own genuine signature. Downstream looks at whether aud is itself and whether act is an allowed caller.
What it looks like in the field
"Just do token exchange with Keycloak" is a statement where you have to look at the version first. Keycloak began officially supporting standard token exchange in 26.2. According to the token-exchange docs, standard exchange (V2) is enabled by default when you start the server, and legacy exchange (V1) is a preview and slated for deprecation, so you have to turn on a flag such as --features=token-exchange together with fine-grained admin permissions v1. V2 also supports only exchange between clients within the same realm and does not support user impersonation or external token exchange. Delegation is classified as an experimental feature in the docs.
The Keycloak in this lab Pod is 26.0.7. That means there is no V2. In fact, token-exchange is not in the discovery document's grant_types_supported, and requesting with that grant returns unsupported_grant_type. So this lab measures service-to-service with Keycloak's client credentials and reproduces the exchange by building an STS yourself. In the field you end up making the same judgment — will you upgrade, will you bet production on a preview feature, or will you put the exchange window in a separate place.
What you will do in the next lab
You get the service token of api-svc from Keycloak, check the signature and claims, and see for yourself that Keycloak 26.0.7 rejects token exchange. Then you stand up a local STS at 8308, swap dev1's token for a token only for orders-api (recording api-svc in act), and have the downstream at 8309 check aud and act. Finally, you prove with real requests that a forged, tampered, or expired subject_token and an unknown audience are all rejected.