Skip to Content
MuoCeon Docs
Third-party OAuth IntegrationReference

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, with typ set to at+jwt in the header.
  • Signing keys: {issuer}/jwks, matched by the header's kid.
  • 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": "..."
}
ClaimMeaning
issAuthorization server identifier; must equal the metadata's issuer verbatim
audAudience, containing both the shorthand (auth) and the resource identifier (https://…/v1/auth)
client_id / cidThe issuing client; both carry the same value, the former is the standard field
principaluser means acting for a user, client means acting for an application (client_credentials)
subStable user identifier; present only on user tokens
didDevice identifier; present only on first-party client tokens
scopeThe effective scopes (already filtered by client capability)
at_versionSession version; increments when a session is revoked or suspended, invalidating old tokens
acr / auth_timeAuthentication strength and time at issuance; they never change afterwards
authorization_detailsFine-grained authorization, present only when the request included it
jti / iat / nbf / expStandard 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 jti should 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
}
  • aud is 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_hash can bind the id_token to the access token obtained in the same exchange.

Validation steps

Do them in order; reject on any failure:

  1. Fetch keys from jwks_uri, verify the signature by kid (RS256).
  2. Verify iss matches the trusted issuer.
  3. Verify aud hits the expectation: resource identifier for access tokens, your own client_id for id_tokens.
  4. Verify exp has not passed and nbf has arrived (allow a small clock skew, e.g. 60 seconds).
  5. 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

OperationEndpointEffect
RevokePOST {issuer}/revokeRevokes the whole session: access and refresh tokens for that user + that client (plus that device for first-party) all become invalid
IntrospectPOST {issuer}/introspectReturns 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

TokenConfidential clientPublic client
Access tokenServer memory / sessionIn-memory variable (never localStorage)
Refresh tokenEncrypted server-side storageSystem keychain / secure storage
id_tokenOnly for reading identity; short-lived caching is fineSame

Never put any token in logs, URLs, or any location reachable via Referer.

On this page