After a successful /oauth/token exchange, your app receives three tokens. Each has a different purpose and lifetime.

Token types

TokenPurposeLifetimeStorage
Access tokenCall /oauth/userinfo and protected APIs~1 hour (expires_in: 3600)Memory or secure server session
Refresh tokenObtain new access tokens without re-loginLong-lived, rotates on useSecure server-side storage only
ID tokenOIDC identity claims (JWT)Same as access tokenParse once at login, do not store long-term
Never store refresh tokens in browser localStorage or expose client secrets in frontend code. Browser apps should use a backend to exchange codes and refresh tokens.

Access token

Use as a Bearer token:
curl https://id.oakwall.mom/oauth/userinfo \
  -H "Authorization: Bearer ACCESS_TOKEN"
When expired, you’ll get 401 invalid_token from /oauth/userinfo. Refresh or re-authenticate.

Refresh token rotation

OakID rotates refresh tokens on every use. The previous token is revoked immediately.
curl -X POST 'https://id.oakwall.mom/oauth/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=refresh_token' \
  -d 'refresh_token=CURRENT_REFRESH_TOKEN' \
  -d 'client_id=YOUR_CLIENT_ID' \
  -d 'client_secret=YOUR_CLIENT_SECRET'
Response includes a new refresh_token. Persist it before discarding the old one.

Rotation failure

If you send an already-used refresh token:
{
  "error": "invalid_grant"
}
The user must sign in again through /oauth/authorize. Events that invalidate all refresh tokens for a client:
  • Client secret regenerated in Developer Portal
  • User sessions revoked for the app
  • User password changed (if applicable)

ID token

The id_token is a JWT signed with HS256. Decode and validate before trusting claims:
ClaimCheck
issMust equal your issuer (https://id.oakwall.mom)
audMust equal your client_id
expMust be in the future
subStable user identifier
import jwt from "jsonwebtoken";

// Server-side only — you need the client secret or a shared validation endpoint
const payload = jwt.decode(idToken, { complete: true });
// Validate iss, aud, exp before using payload.payload.sub
For most apps, calling /oauth/userinfo with the access token is simpler than validating ID tokens locally.

Revocation

Revoke a token when the user signs out:
curl -X POST 'https://id.oakwall.mom/oauth/revoke' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'token=REFRESH_OR_ACCESS_TOKEN' \
  -d 'client_id=YOUR_CLIENT_ID' \
  -d 'client_secret=YOUR_CLIENT_SECRET'
OakID stores user consent per (user_id, client_id). After the first approval, subsequent logins skip the consent screen unless scopes change or consent is revoked.

Best practices

  1. Store refresh tokens server-side — never in the browser
  2. Persist rotated refresh tokens atomically — race conditions cause invalid_grant
  3. Handle invalid_grant gracefully — redirect to login
  4. Revoke on logout — call /oauth/revoke with the refresh token
  5. Use short-lived access tokens — refresh silently in the background