Three ways to find out who a token belongs to, in the order you should reach for them.
Local, offline, no round trip. For anything that received the token itself.
One request, always current. For a client that wants claims and does not want a JWT library.
For a resource server holding a token it did not issue.
Verifying an id token#
An id token is a signed JWT. Verify it against the signing keys from the JWKS URL in the discovery document.
curl -s https://account.ceagle.cc/.well-known/jwks.jsonsignatureRequiredAgainst the key whose
kidmatches the token header. Never trust an unverified token, and never acceptalg: none.issRequiredExactly the issuer from the discovery document.
audRequiredYour client id. A token minted for a different application is not yours to accept.
expRequiredNot expired, with a small clock skew allowance at most.
nonceRequiredMatching the one you sent to the authorize endpoint, if you sent one.
iatSane. A token issued far in the future is a clock problem or an attack.
Cache the key set and refetch when a kid you do not recognise appears. Do not refetch on every
request, and do not pin a single key forever.
Reading claims from userinfo#
/oauth2/userinfoBearercurl -s https://account.ceagle.cc/oauth2/userinfo \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"sub": "8b1e0f42-6d3a-4e9c-9b21-77a5f0c4e1de",
"preferred_username": "ada",
"name": "Ada Lovelace",
"email": "ada@example.com",
"email_verified": true
}Claims are filtered to what the token's scopes allow. sub is always there; the rest depends on
profile and email.
POST behaves identically to GET, for clients that would rather not put a token-bearing request in
a URL-shaped call.
Introspection#
/oauth2/introspectClientFor a resource server that receives a token it did not obtain and needs to know whether it is still good.
curl -s -X POST https://account.ceagle.cc/oauth2/introspect \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d token="$ACCESS_TOKEN"{
"active": true,
"scope": "openid profile email",
"client_id": "…",
"username": "ada",
"token_type": "Bearer",
"exp": 1781452800,
"sub": "8b1e0f42-6d3a-4e9c-9b21-77a5f0c4e1de"
}active is the only field guaranteed to be present. Anything the server will not vouch for answers
{"active": false} without saying why, which is deliberate: a distinct error would tell an attacker
whether a token had ever existed.
sub is the identity#
sub is a UUID, stable for the life of the account, and the same value that appears as user_id on
an Aerie session.
When a token stops working#
| Status | Code | Means |
|---|---|---|
| 401 | invalid_token | Expired, revoked, or never valid. Refresh, and if that fails, sign in again. |
| 403 | insufficient_scope | The token is good but does not carry what this call needs. Check the granted scope rather than retrying. |
Treat a failed refresh as the end of the session rather than as a transient error worth retrying.