Tokens
This page helps you understand the tokens your app gets, and check one before you trust it.
Your app gets up to three tokens from the token endpoint:
- An access token, which your app sends to an API in an
Authorization: Bearerheader. It says what your app may do there. - An ID token, which tells your app who signed in and how. Your app gets one only when it asks for the
openidscope. - A refresh token, which your app trades for new tokens without asking the user to sign in again. See Refresh tokens.
Each one is a JWT (JSON Web Token): a set of claims, statements like “the user is sub”, signed by the auth server. The access token and the ID token are for your app and your APIs to read. The refresh token is for the auth server alone, so treat it as an opaque string.
Check a token
Section titled “Check a token”Your API checks every access token it’s sent, and your app checks the ID token before it trusts who signed in. Most JWT and OpenID Connect libraries do all of this for you once they know the auth server’s discovery URL.
-
Check the signature. Take the key whose
kidmatches the one in the token’s header from the key set at/certs, and verify the token with it. Tokens are signed with RS256. See Discovery and JWKS. -
Check
issis theissuerfrom the discovery document, and thatexphasn’t passed. -
Check
aud. An ID token’s is your client identifier. An access token’s names the resource of each permission it carries, such asproduct-api, so your API checks its own resource identifier is there. -
Check what the token is for:
- An ID token’s
nonceis the one your app sent in the authorization request. - An access token’s
scopeholds the permission the operation needs, such asproduct-api:read.
- An ID token’s
A decoded access token looks like this:
{ "iss": "https://auth.example.com", "sub": "c5b9b6a2-4b39-4b8e-9a8e-6f7f2b3a1d10", "aud": ["authserver", "product-api"], "scope": "openid profile product-api:read", "typ": "Bearer", "iat": 1760000000, "nbf": 1760000000, "exp": 1760000300, "jti": "9a6f1c1e-2d0b-4f3e-8a55-0c1b7a3e2f44", "auth_time": 1759999950, "acr": "urn:goiabada:level2_optional", "amr": ["pwd"], "sid": "0b7e6a31-5f2c-4d8e-9c1a-3e4f5a6b7c8d", "auth_state_generation": 1}How long tokens last
Section titled “How long tokens last”These settings are under Admin, Tokens in the admin console, and apply to every client:
| Setting | Default |
|---|---|
| Token expiration in seconds | 300 (5 minutes) |
| Include OpenID Connect claims in the access token | Off |
| Include OpenID Connect claims in the ID token | On |
The first sets how long both the access token and the ID token last. The other two decide whether user claims such as email or name go in each token, as Scopes describes. A client can override all three on its own Tokens tab: a lifetime of 0 there uses this setting, and each claims switch can be on, off, or left to this setting. See Clients.
Keep the lifetime short. An access token can’t be taken back once it’s issued: an API checking it by signature alone accepts it until exp, whatever happens to the user’s session meanwhile. The lifetime is the longest that can last. See what ending a session doesn’t reach.
An authorization code works once, for 60 seconds. Refresh tokens have lifetimes of their own.
The access token
Section titled “The access token”| Claim | |
|---|---|
iss |
The issuer |
sub |
The user’s subject. In a token from the client credentials flow, the client identifier |
aud |
The resource of each permission scope, and authserver when the scope has an OpenID Connect scope, since /userinfo belongs to it. A string when there’s one, an array when there are more |
scope |
The scopes the token carries |
typ |
Bearer |
iat, nbf, exp |
When it was issued, when it becomes valid, which is the same moment, and when it expires |
jti |
The token’s own identifier |
auth_time, acr, amr |
When and how the user signed in. See ACR and AMR |
sid |
The session the token came from, when there is one. See below |
auth_state_generation |
Reserved. See below |
nonce |
The nonce of the authorization request, when it had one |
User claims, groups and attributes come on top, as Scopes describes. A token from the client credentials flow names no user, so it carries none of auth_time, acr, amr, sid, auth_state_generation or nonce.
sid isn’t always there. It’s in an access token only when the grant it came from is tied to a session. A token from an offline_access grant has none, nor does one from the password grant: those grants are meant to outlive the browser session that started them. Don’t treat a missing sid as an invalid token.
auth_state_generation is reserved. It’s how the auth server tells a token issued before the user’s credentials last changed from one issued after. Don’t parse it, compare it between tokens or build anything on it: its meaning can change.
The ID token
Section titled “The ID token”| Claim | |
|---|---|
iss |
The issuer |
sub |
The user’s subject |
aud |
Your client identifier |
iat, nbf, exp |
When it was issued, when it becomes valid, and when it expires |
jti |
The token’s own identifier |
auth_time |
When the user last entered a credential, a password or a one-time code |
acr, amr |
How the user signed in. See ACR and AMR |
sid |
The session the user signed in with, when there is one. /auth/logout matches on it |
nonce |
The nonce of the authorization request, when it had one |
at_hash |
A hash of the access token issued beside it, in the implicit flow only |
User claims, groups and attributes come on top, as Scopes describes. auth_time is never simply when the token was issued: a sign-in that reused a session, prompt=none included, keeps the time the user last signed in, which is what max_age is measured against.
Which scopes a token gets
Section titled “Which scopes a token gets”The scope your app asks for isn’t always the scope it gets. Once the user has signed in, the auth server drops every permission scope the user doesn’t hold, directly or through a group, without an error. OpenID Connect scopes and offline_access are always kept. If that leaves nothing, the request ends with access_denied, “The user is not authorized to access any of the requested scopes”.
What’s left is what the consent screen asks about, when it’s shown. The user can untick scopes there, and the tokens carry only the ones they approved.
Checked again before a code is issued
Section titled “Checked again before a code is issued”A user can sit on the consent screen for as long as they like, so just before it issues the code, or the implicit flow’s tokens, the auth server checks the request again, in this order:
- The redirect URI is still registered on the client. If it isn’t, the user sees a page, and nothing reaches your app.
- The client is still enabled, or the user sees an error page. The flow is still on for it, or your app gets
unauthorized_client. It’s still allowed to request any administrative scope it asked for, or it getsinvalid_scope. - The user who signed in is the one the
id_token_hintnames, when there was one, or your app getslogin_required, “The authenticated user does not match the id_token_hint”. - The user’s session is still there, still valid, and still theirs, or the sign-in starts again from the password.
- The user is still enabled, or your app gets
access_deniedwith “The user account is disabled.” Their credentials haven’t changed since they signed in, or the sign-in starts again from the password. - The user still holds at least one of the scopes, or your app gets
access_denied.
A silent request can’t start a sign-in again, so it gets login_required, “User authentication is required”, instead.