Skip to content

Verifying tokens

Validate an id token yourself against the JWKS, and use introspection only when you cannot.

Three ways to find out who a token belongs to, in the order you should reach for them.

Verify the id tokenpreferred

Local, offline, no round trip. For anything that received the token itself.

Call userinfosimple

One request, always current. For a client that wants claims and does not want a JWT library.

Introspectlast resort

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.json
What to check
signatureRequired

Against the key whose kid matches the token header. Never trust an unverified token, and never accept alg: none.

issRequired

Exactly the issuer from the discovery document.

audRequired

Your client id. A token minted for a different application is not yours to accept.

expRequired

Not expired, with a small clock skew allowance at most.

nonceRequired

Matching the one you sent to the authorize endpoint, if you sent one.

iat

Sane. 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#

GET/oauth2/userinfoBearer
curl -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#

POST/oauth2/introspectClient

For 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#

StatusCodeMeans
401invalid_tokenExpired, revoked, or never valid. Refresh, and if that fails, sign in again.
403insufficient_scopeThe 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.

CEagle documentation