Scopes and audiences
The photo service has launched a second API. Alongside https://api.photos.example, which serves photos and albums, it now runs a sharing API at https://share.photos.example. The sharing API creates share links, so you can show an album to family without giving them access to your account. Creating a link needs the scope shares.create.
The printer wants to use both. After printing your book, it offers to send a preview of album 42 to the people you choose. Its tokens now have two things to say: what they allow, and where they may be used.
Two questions a token answers
Scope says what kind of access a token carries. photos.read means reading photos. The audience says which API may accept the token. In the JWT access token from Token formats and validation, the audience was the aud claim, set to https://api.photos.example.
The two are easy to conflate, because a scope's name often hints at an API. They do different jobs. A scope is a permission, interpreted by whichever API receives the token. The audience restricts which API should receive it at all, and the photo API checks it during validation, before it looks at scope.
Why should a token for one API not work at another? Every API that receives tokens holds them, at least briefly: in memory, in request logs, in error reports. Suppose the photo service issued the printer one token, good at both APIs and carrying both scopes. The sharing API would then receive tokens that can read your whole photo library. A bug that logged incoming headers, a compromised server, or a careless operator with access to its traffic could replay those tokens at the photo API. The least protected API would set the security of all of them.
With an audience on every token, the sharing API only ever receives tokens addressed to it. If one leaks, it can create share links until it expires, which is bad enough, but it cannot read a single photo, because the photo API rejects any token whose aud does not name it. This is why the JWT access token profile requires an authorization server to give each of its APIs a distinct audience value. Tokens for different APIs must never be interchangeable.
How the audience is chosen
The client does not write the audience. The authorization server sets it, from one of two sources.
The usual source is the requested scope. The photo service knows that photos.read belongs to the photo API and shares.create to the sharing API, so a token requested with photos.read gets https://api.photos.example as its audience. The other source is an explicit request: a client can name the API it wants a token for, using a request parameter designed for that purpose.
A request for both scopes at once raises a problem. Inferring the audience from the scopes would point to two APIs, and one token covering both would bring back the shared token described above. So when the scopes in a request belong to different APIs and the client has not said which API it wants, the JWT access token profile says the server should refuse the request with invalid_scope.
The explicit request is the way through. Once the photo service has added shares.create to the printer's registration, the printer can name both APIs when it sends you to authorize, so you make one decision about the book and the sharing. The code can be redeemed only once, so the printer names the photo API when it exchanges the code, and the sharing API when it later uses its refresh token, receiving a separate access token each time. A client that cannot name the API has to authorize each one separately.
The printer ends up holding two access tokens and sends each only to its own API. That is a little more bookkeeping, and it is the price of making a leak at one API useless at the other.
Designing scopes
The photo service's scopes follow a simple pattern, a resource and an action: photos.read, albums.create, photos.delete. A client developer can guess what each one allows, and a consent screen can describe each in one sentence.
The hard part is granularity. With a single photos scope covering both reading and deleting, the printer would have to ask for the power to empty your library in order to print from it, and you could not offer it less. In the other direction, separate scopes for thumbnails, originals, captions, and locations could each be justified, but a consent screen listing all of them is one people stop reading, and client developers tend to request the whole list anyway.
A useful test is whether a person would decide differently about each scope. Reading and deleting photos pass easily. Thumbnails and originals usually do not. Location data might, because people often feel differently about where a photo was taken than about the photo itself.
Two patterns cause trouble later. The first is the catch-all scope, such as all or admin. Once it exists, clients request it to be safe, and every token that carries it becomes a master key to the account.
The second is putting object identifiers into scope strings, such as album:42:read. Scopes are a fixed vocabulary: the authorization server records which ones each client may request, the consent screen explains them, and the API maps its operations to them. A new string for every album breaks all three. Access to particular objects needs a structured way to describe them.
Advanced OAuth covers the extensions that go further: resource indicators, the standard parameter for naming the API a token is for, and Rich Authorization Requests, which describe access to particular objects such as a single album.
What a token actually allows
Scope is an upper limit, not a promise. What a request can actually do is the overlap of three separate limits, and they are applied at different moments:
| Limit | Where it comes from | For the printer |
|---|---|---|
| The token's scope | What you approved, within what the client asked for | photos.read |
| The subject's own rights | Your account, and what others have shared with it | Your albums, including album 42, and any album shared with you |
| The client's registration | The scopes the photo service allows this client to request, checked before any token is issued | photos.read and shares.create |
Each limit can bind on its own. The printer's token carries photos.read, but it cannot read album 43, because you cannot. If album 43's owner later shares it with you, the printer can read it with the same token: your rights changed, and the token's scope did not need to. If the photo service suspended your account, the token's scope would still say photos.read, but there would be nothing it could read. And the registration caps the scope itself: however willing you were, you could not approve photos.delete for the printer while its registration did not allow that scope, so no token the printer holds could carry it.
The registration and the scope are settled when the token is issued, and checking scope is cheap, which makes it tempting to treat as the whole decision. Only the API can apply the middle row, on every request, because only the API knows whose album 43 is.