A key card, not a passport
A hotel key card is a fair picture of a token. The front desk checks who you are, once, and hands you a card. The card opens your room and the gym, in this hotel, until checkout. The door never asks for your passport. It checks the card.
An access token is the card. The front desk is called an authorization server: the one service that checks who people are, asks what they agree to, and issues tokens. The door is the API.
That is as far as the picture needs to go. What makes a token work is a signature.
What is written on it
Many access tokens are JWTs (said “jots”): three chunks of text joined by dots. You have probably seen one. Not every token is one, and the end of this lesson comes back to that. This lesson uses a JWT because you can read what it says.
eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCJ9HeaderHow it was signedeyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZSIsInN1YiI6ImFsaWNl…ClaimsThe statement itselfq7Zl2mXk3vT9qLz0aB7r…SignatureThe issuer’s seal over the other two partsThe first two chunks are not encrypted. They are JSON, encoded so that they survive a trip through an HTTP header. Decode the middle one and you get a handful of lines, each called a claim:
| In the token | What it says | Which question |
|---|---|---|
"iss": "https://auth.example" | The issuer. The authorization server at auth.example signed this. | Who said they could |
"sub": "alice" | The subject. It is about Alice. | For whom |
"client_id": "calendar-assistant" | It was issued to this app. | Who is acting |
"aud": "https://calendar.example" | The audience. It is meant for the calendar API and for nothing else. | Who said they could |
"scope": "calendar.read" | The scope. It allows reading the calendar. | Who said they could |
"exp": 1794305400 | It stops working at 10:10, ten minutes after it was issued. The number is seconds since 1970. | Who said they could |
The third chunk is the signature. The authorization server computes it from the first two chunks with a private key that never leaves it, and publishes the matching public key. Anyone can check the signature. Only the authorization server can make one.
Try to break it
An API believes nothing in a token until it has checked it. There are five checks. The first button below sends the token as it was issued, and each of the other five does something to it. Before you press one of those, guess which check will catch it. One of them gets past all five.
What happens to the token: Nothing: sent as issued
The token’s claims
{
"iss": "https://auth.example",
"sub": "alice",
"client_id": "calendar-assistant",
"aud": "https://calendar.example",
"scope": "calendar.read",
"exp": 1794305400
}What the API compares them with
this API: calendar.example
time now: 10:04
request: GET /events?day=tomorrow| From an issuer I trust? | Yes | iss is auth.example, which this API is set up to trust. |
|---|---|---|
| Signature intact? | Yes | It verifies with auth.example’s public key. Nothing has changed since it was signed. |
| Addressed to me? | Yes | aud is calendar.example. That is this API. |
| Still in date? | Yes | exp is 10:10. It is 10:04. |
| Covers this request? | Yes | The request reads events, and scope allows calendar.read. |
All five pass, so the API believes the claims: these are Alice’s events, for the app she approved. It did not have to ask anyone at the moment of the request, and it did not have to trust the agent. The signature is the proof.
What happens to the token: Someone edits it
The token’s claims
{
"iss": "https://auth.example",
"sub": "alice",
"client_id": "calendar-assistant",
"aud": "https://calendar.example",
"scope": "calendar.write",
"exp": 1794305400
}What the API compares them with
this API: calendar.example
time now: 10:04
request: DELETE /events/42| From an issuer I trust? | Yes | iss still says auth.example. |
|---|---|---|
| Signature intact? | No | The claims no longer match the signature. Only auth.example’s private key could sign the new version. |
| Addressed to me? | Skipped | Not reached. |
| Still in date? | Skipped | Not reached. |
| Covers this request? | Skipped | Not reached. |
Rejected with 401. Anyone can read a token. Only the service that signs it can change one. Nothing in a token that fails this check is believed, so the API stops here.
What happens to the token: It is sent to another API
The token’s claims
{
"iss": "https://auth.example",
"sub": "alice",
"client_id": "calendar-assistant",
"aud": "https://calendar.example",
"scope": "calendar.read",
"exp": 1794305400
}What the API compares them with
this API: mail.example
time now: 10:04
request: GET /messages| From an issuer I trust? | Yes | The mail API trusts auth.example too. |
|---|---|---|
| Signature intact? | Yes | Unchanged since it was signed. |
| Addressed to me? | No | aud is calendar.example. This is mail.example. |
| Still in date? | Skipped | Not reached. |
| Covers this request? | Skipped | Not reached. |
Rejected. A token is addressed to one API, and every API has to read the address. If the mail API skipped this check, any service that was ever handed a token could turn around and use it somewhere else.
What happens to the token: It is used an hour later
The token’s claims
{
"iss": "https://auth.example",
"sub": "alice",
"client_id": "calendar-assistant",
"aud": "https://calendar.example",
"scope": "calendar.read",
"exp": 1794305400
}What the API compares them with
this API: calendar.example
time now: 11:04
request: GET /events?day=tomorrow| From an issuer I trust? | Yes | iss is auth.example. |
|---|---|---|
| Signature intact? | Yes | Unchanged since it was signed. |
| Addressed to me? | Yes | aud is calendar.example. |
| Still in date? | No | exp was 10:10. It is 11:04. |
| Covers this request? | Skipped | Not reached. |
Rejected with 401. Access tokens are short-lived on purpose, often a few minutes. The agent can get a fresh one without sending Alice through sign-in again; the next lesson shows how.
What happens to the token: The agent tries to delete an event
The token’s claims
{
"iss": "https://auth.example",
"sub": "alice",
"client_id": "calendar-assistant",
"aud": "https://calendar.example",
"scope": "calendar.read",
"exp": 1794305400
}What the API compares them with
this API: calendar.example
time now: 10:04
request: DELETE /events/42| From an issuer I trust? | Yes | iss is auth.example. |
|---|---|---|
| Signature intact? | Yes | Unchanged since it was signed. |
| Addressed to me? | Yes | aud is calendar.example. |
| Still in date? | Yes | exp is 10:10. It is 10:04. |
| Covers this request? | No | Deleting needs calendar.write. Alice approved calendar.read. |
Refused with 403. The token is genuine, and it does not cover this. It does not matter why the agent tried: a bug, a bad instruction, or something it read on a web page. The limit holds.
What happens to the token: Someone else sends a copy
The token’s claims
{
"iss": "https://auth.example",
"sub": "alice",
"client_id": "calendar-assistant",
"aud": "https://calendar.example",
"scope": "calendar.read",
"exp": 1794305400
}What the API compares them with
this API: calendar.example
time now: 10:04
request: GET /events?day=tomorrow| From an issuer I trust? | Yes | iss is auth.example. |
|---|---|---|
| Signature intact? | Yes | Unchanged since it was signed. |
| Addressed to me? | Yes | aud is calendar.example. |
| Still in date? | Yes | exp is 10:10. It is 10:04. |
| Covers this request? | Yes | scope allows calendar.read. |
All five pass, and the API answers. Nothing in this token says who is allowed to send it. A token like this is called a bearer token: it works for whoever holds it, like cash. A short life limits how long a stolen copy is useful. The final lesson shows how to make a copy worth far less.
| What happens to the token | From an issuer I trust? | Signature intact? | Addressed to me? | Still in date? | Covers this request? |
|---|---|---|---|---|---|
| Nothing: sent as issued | Yes | Yes | Yes | Yes | Yes |
| Someone edits it | Yes | No | Skipped | Skipped | Skipped |
| It is sent to another API | Yes | Yes | No | Skipped | Skipped |
| It is used an hour later | Yes | Yes | Yes | No | Skipped |
| The agent tries to delete an event | Yes | Yes | Yes | Yes | No |
| Someone else sends a copy | Yes | Yes | Yes | Yes | Yes |
Do not write the signature check yourself. Use a maintained library, and tell it in your own configuration which issuer, which audience, which keys and which signing method to accept. Never take those from the token: one that brings its own key, says where to fetch one, or says it needs no signature must be refused. Tell it which kind of token to accept as well. The same service signs other things, such as the ID token an app gets at sign-in, and a check that passes anything it signed would let one of those be used as an access token. A token in this format says what it is in its header: at+jwt, which may also be written application/at+jwt. The API must require one of the two. The last of the five, whether the token covers this request, is yours to write: only your code knows what each request needs. Decoding a token is not checking it.
When the API says no
The refusals in the figure are of two kinds, worth telling apart because they send you to different places.
- 401 means the API did not accept the token at all. One of the first four checks failed, or no token arrived. If sign-in has just succeeded and the first call still fails, a common cause is the audience: the token was issued for a different address from the one the API knows itself by. Fix what the token is requested for, and never switch the audience check off to make the error go away. Check, too, that the token is being sent.
- 403 means the token is good and does not cover this request. Retrying will not help. The agent needs a wider grant, and that means asking whoever can give one.
What a valid token does not prove
The last button is the uncomfortable one. A token records what was granted. It does not record who is allowed to carry it. A copy taken from a log file, a debugging session or an agent’s memory works exactly as well as the original, until it expires.
That is the trade a bearer token makes: simple to use, and only as safe as the places it passes through. It is why tokens are short-lived, why each is addressed to one API, and why it matters so much where an agent keeps them.
One more thing you will run into. Not every token can be read. Some are random strings that mean nothing by themselves, and the API asks the authorization server what each one stands for. Most of the checks then happen there, and comparing the scope with the request stays with the API. Asking is also how an API learns that a token was withdrawn early: one that only checks the token itself goes on accepting a withdrawn token until it expires.
Which kind a token is lies between the authorization server and the API, and they can change it at any time. So your agent sends a token on as it received it, and never depends on reading one.
Check yourself
Answer before you read on. Nothing is recorded.
What to keep
- A token is a statement signed by an authorization server: who it is about, which app it was given to, which API it is for, what it allows, and until when.
- The API checks five things before believing any of it: issuer, signature, audience, expiry and scope.
- Audience is the check that gets forgotten. A token is addressed to one API.
- A valid token proves what was granted. It does not prove who is sending it: a plain token is a bearer token.
The names for these
The rest of this guide, and every specification, uses these terms. Each links to its entry in the glossary.
| In this lesson | Its name | Defined as |
|---|---|---|
| The service that checks who people are and issues tokens | Authorization server | The system that authenticates users and clients, records consent, and issues tokens. |
| The token an API accepts | Access token | A credential an API accepts as proof that a request is authorized. Short-lived, addressed to one audience, and limited to particular scopes. |
| One line inside a token | Claim | One named statement inside a token, such as sub (who it is about) or exp (when it expires). The issuer’s signature covers all of them. |
| Who signed it | Issuer | The identifier of the system that issued a token. Verifiers check it against the issuers they trust. |
| Who it is about | Subject | Who or what a token is about: the person an agent is acting for, or the agent itself when it acts alone. |
| Which API it is for | Audience | The recipient a token is intended for. A resource must reject tokens whose audience is someone else; this is the main defense against a token being replayed at a different service. |
| What it allows | Scope | A name for a permission that a token carries, such as calendar.read. Scopes are coarse on purpose: they name kinds of access, not single actions. |
| Three chunks of text joined by dots | JWT | A compact set of claims, usually signed and written as three chunks of text joined by dots. The common format for access tokens and ID tokens. |
| A token that works for whoever holds it | Bearer token | A token that works for whoever presents it, like cash. A sender-constrained token is the kind that does not. |
Go deeper
The same ground at full depth, when you want it. None of it is needed for the next lesson.
- What is in a tokenNine credentials you will meet, claim by claim.
- JWT Profile for Access TokensThe specification the example token follows, RFC 9068.