Service-to-service access
The photo API is rarely the end of the line. It validates the printer's token and confirms that album 42 is yours, but the image files themselves live in an internal storage service at https://storage.photos.example. To answer the printer, the photo API has to make a request of its own.
That second request crosses another trust boundary. The storage service has to decide whether to hand over the files for album 42, and the photo API has to present something that lets it decide.
A call behind the API
Start with what the photo API already holds, the printer's access token. Whether the API decodes it as a JWT or receives the same fields from introspection, it knows these claims. All domains, tokens, and keys in these examples are fictional.
{
"iss": "https://auth.photos.example",
"sub": "user-2048",
"aud": "https://api.photos.example",
"client_id": "photo-printer",
"scope": "photos.read",
"iat": 1790845200,
"exp": 1790845800,
"jti": "demo-token-id-7"
}
Most of what the storage service would want to know is here: whose photos these are (user-2048), which application asked (photo-printer), and what kind of access you approved (photos.read). One claim stands in the way. aud names the photo API, and the Scopes and audiences lesson explained why a different API should refuse a token issued for someone else.
The photo API needs a way to call storage that still gives storage enough to make its own decision. Several approaches are common, and the obvious ones each lose something.
Shortcuts that lose something
Calling as itself. The photo API is registered with the authorization server as the confidential client photo-api, so it could use the client credentials grant and present a token that represents only the photo API. Storage then knows which service is calling but not for whom. It has to trust the photo API to have checked that album 42 is yours, and the photo API's token has to be able to read every user's files, because it might be asked for anyone's. A single mistake in the photo API's object checks, the broken object-level authorization described in Enforcing access at an API, now exposes the whole storage service with no second check behind it. Client credentials remain the right choice for calls that really are the photo API's own business, such as reporting how much space it uses. They are the wrong choice for calls about your photos.
Forwarding the incoming token. The photo API could pass the printer's token along unchanged. A storage service that validates tokens properly rejects it, because its audience is the photo API. The tempting fixes, issuing tokens with both audiences or switching off storage's audience check, make every token issued for the photo API valid at storage. The printer, which was never meant to talk to storage, could then call it directly, and any service that receives such a token, including a compromised or careless one, could replay it there. The token's scope is also the wrong description: it says what the printer may do at the photo API, not what the photo API needs from storage.
Naming the user in a header. The photo API could call storage with its own token and add a header such as X-User-Id: user-2048. Storage now has a user to check, but only the caller's word for it. Any service able to call storage could name any user, and a bug that lets an outside request set that header, or pass it through unchanged, would let a stranger name you. A downstream service should not accept a user identity that its caller merely asserts.
A token for the next hop
The approach that keeps both the user and the audience straight is to ask the authorization server for a new token. The photo API authenticates as photo-api, presents the printer's token, and asks for a token meant for the storage service. The authorization server checks that the presented token is valid and was issued by itself, that photo-api is allowed to make this kind of request, and that storage is a service it may ask for. It then issues a token that:
- names
https://storage.photos.exampleas its audience, so it works nowhere else; - keeps
user-2048as its subject, so storage can check which files belong to you; - carries only the access this call needs, and never more than the original token allowed;
- records that the photo API is acting on your behalf, so storage and its logs can see the chain;
- expires within minutes.
Storage validates it like any access token: issuer, audience, and expiry, then whether the files requested belong to user-2048. If a bug in the photo API asked for album 43, which belongs to someone else, storage's own check would refuse, because the token speaks only for you. This mechanism is called token exchange, and it is a grant at the token endpoint, like the others you have met.
Each exchange is another request to the authorization server, so a service usually keeps an exchanged token for its short lifetime and reuses it for the same user and audience, rather than exchanging before every call.
Credentials for the services themselves
In every approach above, the photo API has to prove who it is, to the authorization server or to storage. Inside a company, the easy way is a long-lived client secret copied into each service's configuration. It works, and it has every weakness that Secrets and signed assertions described, multiplied by the number of services and copies.
Many hosting platforms offer something better. The platform knows which service it is running, because it started it. It can give the photo API an identity as a service, and each running instance a short-lived signed credential that states that identity, often a JWT that expires within the hour. An identity managed this way for a piece of software is usually called a workload identity. The photo API presents that credential when it authenticates to the authorization server, much like the signed client assertion the printer used, except that the platform signed it rather than the service holding a key of its own. The authorization server accepts it because it has been configured in advance to trust the platform as an issuer for photo-api, the same kind of arrangement Lantern Studio made for its designers. There is no secret to copy, leak, or rotate by hand, because the platform keeps issuing fresh credentials.
Services can also prove their identity to each other directly with mutual TLS, in which both ends of a connection present certificates, so storage knows the caller is the photo API before it reads a single header. Many internal platforms set this up automatically between services. A token can even be bound to the certificate of the service it was issued to, so a copied token is useless without the matching private key.
The same questions have come back throughout these architectures in different forms: which component is the client, where its credentials and tokens live, and what someone who compromises one part can carry away. Security and failure cases turns those questions around and follows attackers who exploit a missing answer, from loosely matched redirect URIs to stolen refresh tokens, before Advanced OAuth returns to token exchange and mutual TLS in full.