Token Lifecycle
Token Types
Keymaster issues three types of tokens:
| Token | Format | Lifetime | Storage |
|---|---|---|---|
| Access Token | RS256 JWT | 15 min (configurable per app) | Client-side or session store |
| Refresh Token | Opaque string (64 hex chars) | 30 days (configurable per app) | Server-side session store only |
| SSO Session | Opaque string (httponly cookie) | 8 hours | km_sso cookie on Keymaster domain |
Obtaining the First Token Pair (Login)
Apps start the login flow by redirecting the user to the authorize endpoint:
GET {KEYMASTER_URL}/login?app_id=<APP_ID>&redirect_uri=<URI>
Optional parameters: state (echoed back), prompt=login (force re-authentication), and PKCE (code_challenge=<S256>&code_challenge_method=S256).
How the credential is delivered back depends on the redirect scheme:
| Redirect scheme | Callback delivers | Next step |
|---|---|---|
http / https (web) |
?code=<ONE_TIME_CODE> |
Exchange the code at POST /token/exchange for the token pair |
| Native custom scheme (mobile) | ?access_token=…&refresh_token=… on the deep link |
Use the tokens directly |
| Any error | ?error=<reason> |
Handle the failure |
Web apps must exchange the code — they never receive tokens directly on the callback URL. This avoids leaking tokens through browser history, referrers, and server logs.
POST {KEYMASTER_URL}/token/exchange
{
"code": "<ONE_TIME_CODE>",
"app_id": "your-app-id",
"code_verifier": "<original-verifier>" // required only if you sent a code_challenge
}
→ {
"access_token": "eyJhbG...",
"refresh_token": "a1b2c3...",
"token_type": "Bearer",
"expires_in": 900
}
The one-time code is single-use and expires 60 seconds after issuance. Once you hold the token pair, the refresh/rotation lifecycle below is identical for web and native.
Access Token (JWT)
Claims
{
"sub": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"name": "Jane Doe",
"aud": "a56e4998-e65d-4817-b69d-009ab7dee28f",
"iss": "https://keymaster.cloud-monitor.com",
"iat": 1710547200,
"exp": 1710548100,
"roles": ["user", "admin"],
"token_type": "access"
}
| Claim | Description |
|---|---|
sub |
User UUID (stable, unique per user across all apps) |
email |
User's email address |
name |
Display name (may be null) |
aud |
App UUID this token was issued for |
iss |
Keymaster base URL |
iat / exp |
Issued-at and expiration timestamps |
roles |
Array of roles for this user in this app |
token_type |
Always "access" — distinguishes user access tokens from service tokens |
Verification
Always verify access tokens locally using JWKS:
GET https://keymaster.cloud-monitor.com/.well-known/jwks.json
Check: alg=RS256, iss matches Keymaster URL, aud matches your app_id, exp is in the future.
Do NOT call Keymaster on every request. Cache the JWKS keys (refresh hourly or when kid doesn't match).
Alternative: Server-Side Verification
If you can't verify JWTs locally:
POST /token/verify
{
"token": "eyJhbG...",
"audience": "your-app-id"
}
→ { "valid": true, "claims": { ... } }
→ { "valid": false, "error": "Token has expired" }
audience is required — pass your app_id. The endpoint validates it against the token's aud claim and rejects service tokens (token_type: "service") presented as user tokens.
Refresh Token Rotation
Refresh tokens are rotated on every use. When you exchange a refresh token, the old one is revoked and a new pair (access + refresh) is issued.
POST /token/refresh
{
"refresh_token": "a1b2c3d4...",
"app_id": "your-app-id"
}
→ {
"access_token": "new-jwt...",
"refresh_token": "new-refresh-token...",
"token_type": "Bearer",
"expires_in": 900
}
Critical: You must store the new refresh token. The old one is now invalid.
Replay Detection
If a revoked refresh token is reused (potential token theft), Keymaster revokes ALL refresh tokens for that user+app combination as a security precaution. This forces the user to re-authenticate.
Correct App Integration Pattern
Store both tokens in a durable session store (database, not memory):
# On each authenticated request:
# 1. Look up session in DB
# 2. Decode access JWT exp claim locally (no network call)
# 3. If expired (or within 30 seconds of expiry):
# → POST /token/refresh → update both tokens in DB
# 4. If refresh returns 401:
# → Token revoked/expired → redirect to login
# 5. If network error:
# → Degrade gracefully (user was previously authenticated)
DO NOT: - Store tokens in memory (lost on server restart) - Use the access token as a one-shot identity check and then ignore it - Forget to update the refresh token after rotation - Set a short cookie expiry that doesn't match the refresh token lifetime
DO:
- Set cookie max_age to 30 days (match refresh token lifetime)
- Refresh proactively (30-second buffer before expiry)
- Handle refresh failure gracefully (redirect to login, don't crash)
Token Revocation
Revoking a Refresh Token (Logout)
POST /token/revoke
{
"refresh_token": "a1b2c3d4..."
}
→ { "status": "ok" }
Always revoke on logout. This prevents the token from being used even if it was intercepted.
Password Change
When a user changes their password, Keymaster automatically revokes all SSO sessions for that user. They must re-authenticate everywhere. Refresh tokens for individual apps are NOT automatically revoked — the app will continue working until the refresh token expires or the user explicitly logs out.
Service Tokens (Client Credentials)
For server-to-server authentication, apps use the client credentials grant:
POST /auth/token
grant_type=client_credentials
client_id={app_id}
client_secret={secret}
scope=push:send
Service tokens are distinct from user tokens:
- token_type: "service" claim (prevents type confusion; not accepted as a user token)
- sub is the app_id, not a user_id; there is no aud claim
- carries a scope claim (space-separated granted scopes)
- 15-minute expiry (expires_in 900), no refresh — just re-authenticate
- Scoped to specific capabilities (e.g., push:send). The app is only granted scopes it is entitled to — each requested scope must be listed in the app's config["service_scopes"], otherwise the request returns 400 invalid_scope.
See Server-to-Server Guide for details.
Token Lifetimes at a Glance
| Scenario | What expires | What to do |
|---|---|---|
| Access JWT expires (15 min) | Access token | Call /token/refresh |
| Refresh token expires (30 days) | Refresh token | Redirect to Keymaster login |
| User idle for 30+ days | Both tokens | Redirect to Keymaster login |
| SSO session expires (8 hours) | km_sso cookie |
User sees login screen on next app switch |
| Password changed | All SSO sessions | User re-authenticates everywhere |
| Refresh token replayed | ALL tokens for user+app | User re-authenticates |