Audience restrictions and validation
The printer now names the API it wants each token for. That request achieves nothing on its own. It protects you only if the authorization server writes the right audience into the token and refuses requests it should not serve, and if every API refuses a token that does not name it.
The Token formats and validation lesson placed the audience check among the API's validation steps. With resource indicators, each side has a few more decisions to make.
Setting the audience
When a client names a resource, the authorization server should restrict the token to it. In a JWT access token, that restriction is the aud claim, and an introspection response reports it in a member with the same name. Normally the value is the requested resource itself, as Selecting the intended resource noted. The specification for resource indicators also lets a server map the requested value to another identifier it uses for that API, such as a more general URI. The photo service keeps things simple and writes the resource value unchanged, so the photo API's tokens carry "aud": "https://api.photos.example".
Whatever mapping a server uses, each API still needs its own audience value, the requirement the Scopes and audiences lesson noted in the JWT access token profile. A mapping that sent two APIs to the same identifier would make their tokens interchangeable, however carefully each API checked.
Refusing a resource
Resource indicators come with one error, invalid_target. A server returns it when the requested resource is invalid, unknown, or malformed, when a required resource is missing, or when the combination of resources and scopes makes no sense. All domains in these examples are fictional. At the token endpoint, it arrives like any other token error:
HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store
{
"error": "invalid_target"
}
At the authorization endpoint, the error returns through the browser to the client's redirect URI, with the attempt's state and iss, like the other authorization errors. Either way, retrying the same request will not help. Something is wrong with the client's configuration, or with where its values came from.
A server that knows every API it issues tokens for can refuse any other resource, and that closes a gap the Token theft and replay lesson described: the counterfeit API. Imagine a version of the printer that lets you connect a photo service by typing its address, and an attacker who persuades you to enter https://photos-api.attacker.example. A printer that asks for a token for that resource gets nothing from a careful authorization server, because it does not recognize the resource. And if a careless server did issue one, its audience would name the attacker's address. When the attacker replayed it at the real photo API, the API would see that the token was not meant for it and refuse.
That protection depends on the resource value being the API's real address. A client that sends tokens to https://api.photos.example can check through TLS that it is talking to that host. When a server uses abstract identifiers instead, the client has to learn by some other trusted route which addresses belong to each identifier.
Checking the audience at the API
The photo API knows its own identifier from its configuration. It never takes the expected value from the incoming request, such as its Host header, because the caller controls what the request says. The aud claim can hold a single string or an array of strings, and the API accepts the token only when its own identifier is one of them:
const PHOTO_API = 'https://api.photos.example';
function audienceAccepted(aud: unknown): boolean {
const audiences = typeof aud === 'string' ? [aud]
: Array.isArray(aud) ? aud
: [];
return audiences.includes(PHOTO_API);
}
The comparison is exact. There is no prefix match, no case folding, and no partial credit for a token from the same authorization server. A token whose audience is https://share.photos.example fails here, even though the same server signed it with the same key. When the check fails, the API answers as it does for any token it cannot accept:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token"
An API that relies on introspection makes the same comparison with the aud member of the response. A token being active means only that the server still honors it, not that it was issued for this API.
Two further questions follow the basic check. First, do the token's scopes or authorization details mean anything here? A token that names this API but carries only scopes for another one has nothing to authorize. Second, does the token name other APIs as well? A token with several audiences means another API holds a working copy. An API can accept such tokens, refuse them, or accept them only for less sensitive operations, as its own policy decides.
What the audience does not cover
Token theft and replay followed a copied token and showed what an audience leaves open. It limits where a token works, not who presents it, so anyone who copies a token for the photo API can use it at the photo API until it expires. And a compromised photo API can still use the tokens it receives there, although they work nowhere else.
The audience also depends on the client sending each token only to the API it names, as Using access tokens described. If the printer sent its photo token to the sharing API, the sharing API would correctly refuse it, but a party the audience was meant to exclude would already hold a copy.
Who presents a token is the question that binding answers. A token bound to the client that holds it is useless to anyone else, and the later groups on Demonstrating Proof of Possession (DPoP) and Mutual TLS follow the two standard ways of binding one. Together with audience restriction, they shrink what a copied token is worth: it works at one API, for one client, for a short time.