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