Tokens
Access, refresh and id token structure, claim meanings and validation steps.
MuoCeon issues three kinds of token: access token, refresh token, id_token. Access tokens and id_tokens are signed JWTs you can verify locally; refresh tokens are opaque to clients — do not parse them.
Access token
- Algorithm:
RS256, withtypset toat+jwtin the header. - Signing keys:
{issuer}/jwks, matched by the header'skid. - Lifetime: 1 hour for confidential clients, 30 minutes for public clients.
{
"iss": "https://id.example.com/v1/oauth",
"aud": ["auth", "https://id.example.com/v1/auth"],
"client_id": "app_9f3c",
"cid": "app_9f3c",
"principal": "user",
"sub": "0192f3a4-...",
"scope": "openid profile email",
"at_version": 1,
"acr": "urn:muoceon:acr:aal2",
"auth_time": 1760000000,
"iat": 1760000000,
"nbf": 1759999995,
"exp": 1760003600,
"jti": "..."
}| Claim | Meaning |
|---|---|
iss | Authorization server identifier; must equal the metadata's issuer verbatim |
aud | Audience, containing both the shorthand (auth) and the resource identifier (https://…/v1/auth) |
client_id / cid | The issuing client; both carry the same value, the former is the standard field |
principal | user means acting for a user, client means acting for an application (client_credentials) |
sub | Stable user identifier; present only on user tokens |
did | Device identifier; present only on first-party client tokens |
scope | The effective scopes (already filtered by client capability) |
at_version | Session version; increments when a session is revoked or suspended, invalidating old tokens |
acr / auth_time | Authentication strength and time at issuance; they never change afterwards |
authorization_details | Fine-grained authorization, present only when the request included it |
jti / iat / nbf / exp | Standard token identifier and time window |
acr and auth_time are snapshots
Once issued, a token never changes: later step-up does not strengthen an old token, and refreshing does
not make auth_time newer. A judgement of "authenticated just now" must be based on a newly issued token.
Refresh token
- Shape: a signed JWT, but opaque to clients — neither its internal structure nor its
jtishould be relied on. - Lifetime: 30 days for confidential clients, 14 days for public clients; refreshing never extends past the cap.
- Rotation: every refresh yields a new one and the old value stops working immediately; the session's old access token is invalidated as well.
Do not parse, concatenate, or assume the length. Store it, and submit it verbatim when refreshing.
id_token
Issued only when exchanging an authorization code whose scope includes openid.
{
"iss": "https://id.example.com/v1/oauth",
"sub": "0192f3a4-...",
"aud": "app_9f3c",
"exp": 1760003600,
"iat": 1760000000,
"at_hash": "...",
"auth_time": 1760000000
}audis your client_id, not a resource audience — deliberately different from the access token.- It carries protocol claims only; nickname, email and the like are not inside it — use UserInfo.
at_hashcan bind the id_token to the access token obtained in the same exchange.
Validation steps
Do them in order; reject on any failure:
- Fetch keys from
jwks_uri, verify the signature bykid(RS256). - Verify
issmatches the trusted issuer. - Verify
audhits the expectation: resource identifier for access tokens, your ownclient_idfor id_tokens. - Verify
exphas not passed andnbfhas arrived (allow a small clock skew, e.g. 60 seconds). - Call UserInfo when you need profile data; do not try to read nickname or email out of the access token.
UserInfo expects the auth audience
https://id.example.com/v1/auth/userinfo requires an auth audience token and at least one of the
openid / profile / email scopes. An oauth audience token always fails there.
Revocation and introspection
| Operation | Endpoint | Effect |
|---|---|---|
| Revoke | POST {issuer}/revoke | Revokes the whole session: access and refresh tokens for that user + that client (plus that device for first-party) all become invalid |
| Introspect | POST {issuer}/introspect | Returns whether the token is active and its metadata; confidential clients only |
The revocation endpoint returns 200 for tokens that do not exist — do not use it to test token existence. After revocation your access token becomes invalid immediately, including the one you were about to use.
Where to store tokens
| Token | Confidential client | Public client |
|---|---|---|
| Access token | Server memory / session | In-memory variable (never localStorage) |
| Refresh token | Encrypted server-side storage | System keychain / secure storage |
| id_token | Only for reading identity; short-lived caching is fine | Same |
Never put any token in logs, URLs, or any location reachable via Referer.