Token Reference
UniAuth issues several types of tokens, each serving a distinct purpose in the authentication and authorization lifecycle. This reference documents the format, claims, lifetime, and security characteristics of every token type, along with guidance on how to handle them in your application.
Token Overview
The following table provides a high-level comparison of all token types issued by UniAuth:
| Token | Format | Algorithm | Lifetime | Recommended Storage |
|---|---|---|---|---|
| Session Token | JWT | HS256 | 7 days | httpOnly cookie (managed by UniAuth) |
| Access Token | JWT | HS256 | 1 hour | In-memory (client application) |
| ID Token | JWT | RS256 | 1 hour | In-memory (client application) |
| Refresh Token | Opaque string | N/A | 30 days | Secure server-side storage |
| Authorization Code | Opaque string | N/A | 10 minutes | One-time use (not stored) |
Session Token
The session token is issued when a user signs in directly to UniAuth (via the login page, social login, or passkey). It is stored as an httpOnly, Secure, SameSite cookie named auth_token. This cookie is managed entirely by UniAuth and is not accessible to client-side JavaScript.
Claims
| Claim | Type | Description |
|---|---|---|
id | string (UUID) | The user's unique identifier |
email | string | The user's email address |
first_name | string | First name (may be null) |
last_name | string | Last name (may be null) |
avatar_url | string | URL to the user's avatar image (may be null) |
role | string | The user's role (e.g., "user", "admin", "moderator") |
amr | string[] | Authentication Methods Reference — list of methods used during login |
auth_time | number | Unix timestamp of when the user authenticated |
AMR Values
The amr (Authentication Methods Reference) claim lists the authentication methods that were used during the login session:
| Value | Meaning |
|---|---|
pwd | Password-based authentication |
mfa | Multi-factor authentication was completed |
otp | Time-based one-time password (authenticator app) |
sms | SMS verification code |
hwk | Hardware key / passkey (WebAuthn) |
A login with password and TOTP verification would have amr: ["pwd", "otp", "mfa"]. A passkey login would have amr: ["hwk"].
OAuth Access Token
Access tokens are issued by the token endpoint after a successful authorization code exchange or refresh token grant. They are used to authenticate requests to the UserInfo endpoint and any resource server that accepts UniAuth tokens.
Important: Treat access tokens as opaque strings. While they are JWTs that can be decoded, client applications should not rely on parsing their claims. Use the /api/oauth/userinfo endpoint to retrieve user information. Token claims may change between versions without notice.
Claims
| Claim | Type | Description |
|---|---|---|
sub | string | App-specific pairwise subject identifier (not the user's real ID) |
aud | string | The client ID of the application this token was issued to |
scope | string | Space-separated list of granted scopes |
jti | string | Unique token identifier |
iss | string | Issuer URL (your UniAuth instance) |
iat | number | Unix timestamp when the token was issued |
exp | number | Unix timestamp when the token expires (iat + 3600) |
Using Access Tokens
Include the access token in the Authorization header as a Bearer token:
GET /api/oauth/userinfo
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
HTTP/1.1 200 OK
Content-Type: application/json
{
"sub": "2b7f16373f76cbdf9c0241ae59903ee2d143830d4edf74693b608fd2357cc219",
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe",
"email": "[email protected]",
"email_verified": true,
"picture": "https://uniauth.id/avatars/jane.jpg"
}OAuth ID Token
The ID token is a cryptographically signed JWT that contains claims about the authenticated user. It is issued alongside the access token during authorization code exchange and refresh token grants (when the openid scope is requested). ID tokens are signed with RS256 and can be verified using the public key from the JWKS endpoint.
Decoded Example
// Header
{
"alg": "RS256",
"typ": "JWT",
"kid": "uniauth-oidc-key"
}
// Payload
{
"iss": "https://uniauth.id",
"sub": "2b7f16373f76cbdf9c0241ae59903ee2d143830d4edf74693b608fd2357cc219",
"aud": "your-client-id",
"exp": 1709049600,
"iat": 1709046000,
"auth_time": 1709045900,
"nonce": "abc123random",
"amr": ["pwd", "otp", "mfa"],
"sid": "session-id-hash",
"at_hash": "LDktKdoQak3Pk0cnXxCltA",
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe",
"preferred_username": "janedoe",
"email": "[email protected]",
"email_verified": true,
"picture": "https://uniauth.id/avatars/jane.jpg"
}Scope-Based Claims
The claims included in the ID token depend on the scopes granted during authorization. The following table maps each scope to the claims it enables:
| Scope | Claims Included |
|---|---|
openid | sub, aud, iss, auth_time, amr, sid, at_hash, nonce |
profile | name, given_name, family_name, preferred_username, picture, profile, website, gender, birthdate, locale, zoneinfo, updated_at |
email | email, email_verified |
phone | phone_number, phone_number_verified |
address | address (structured object with street_address, locality, region, postal_code, country) |
Refresh Token
Refresh tokens are opaque strings (48 random bytes, base64url-encoded) that allow your application to obtain new access tokens without requiring the user to re-authenticate. They have a 30-day lifetime and are stored securely on UniAuth's servers as a cryptographic hash.
Refresh Token Rotation
UniAuth enforces refresh token rotation. Every time you exchange a refresh token for a new access token, the response includes a new refresh token. The previous refresh token is immediately invalidated. You must store and use the new refresh token for your next token refresh.
POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=CURRENT_REFRESH_TOKEN
&client_id=your-client-id
&client_secret=your-client-secret
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "NEW_REFRESH_TOKEN_MUST_STORE_THIS",
"id_token": "eyJhbGciOiJSUzI1NiIs..."
}Replay Detection
UniAuth implements family-based replay detection to protect against refresh token theft. All refresh tokens issued from the same authorization are linked in a "token family." If a previously-used (rotated) refresh token is presented again, UniAuth assumes token theft has occurred and takes the following actions:
- Revokes all refresh tokens in the token family
- The legitimate client and the attacker both lose access
- The user must re-authenticate to obtain new tokens
Warning: Never cache or store multiple copies of refresh tokens. Always replace the stored token with the new one from each refresh response. Using a stale token will revoke the entire token family.
Authorization Code
Authorization codes are short-lived, single-use credentials that are exchanged for tokens at the token endpoint. They are returned to your application's redirect URI after the user grants consent.
- Lifetime: 10 minutes from issuance
- Usage: Single-use only. Attempting to reuse an authorization code will fail and may trigger revocation of any tokens already issued from that code.
- PKCE required: All authorization requests must include a
code_challengeparameter using the S256 method. The correspondingcode_verifiermust be sent during the token exchange.
// Step 1: Generate PKCE values
const codeVerifier = generateRandomString(128);
const codeChallenge = base64url(sha256(codeVerifier));
// Step 2: Redirect to authorization endpoint
const authUrl = new URL("https://uniauth.id/api/oauth/authorize");
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("client_id", "your-client-id");
authUrl.searchParams.set("redirect_uri", "https://your-app.com/callback");
authUrl.searchParams.set("scope", "openid profile email");
authUrl.searchParams.set("state", csrfToken);
authUrl.searchParams.set("nonce", randomNonce);
authUrl.searchParams.set("code_challenge", codeChallenge);
authUrl.searchParams.set("code_challenge_method", "S256");
window.location.href = authUrl.toString();
// Step 3: Exchange code for tokens at your callback
// POST /api/oauth/token with grant_type=authorization_code,
// code=RECEIVED_CODE, code_verifier=codeVerifierPairwise Subject Identifiers
UniAuth uses pairwise subject identifiers as a core privacy feature. Instead of exposing a user's real identifier to every application, each application receives a unique, application-specific identifier for every user. The same user will have completely different sub values across different applications.
Same user, different applications:
App A receives:
sub: "2b7f16373f76cbdf9c0241ae59903ee2d143830d4edf74693b608fd2357cc219"
App B receives:
sub: "6a928c03d0e6cb00a35a31f769348d20a15e20eba64cdba9f088fac9917e18b7"
App C receives:
sub: "9f4e2d8a1b3c5e7f0a2b4d6e8f1a3c5e7b9d1f3a5c7e9b1d3f5a7c9e1b3d5f"This design prevents applications from correlating users across different services. Even if two applications compared their user databases, they would have no way to determine whether any two identifiers belong to the same person.
The pairwise sub is deterministic: the same user and the same application will always produce the same identifier. Use it as a stable per-application user identifier for linking accounts in your database.
The sub claim appears in access tokens, ID tokens, and UserInfo responses. The user's real internal identifier is never exposed to OAuth client applications.
Token Validation
ID tokens should be validated before trusting their claims. The following steps describe how to properly validate an ID token:
- Fetch the JWKS. Retrieve the public signing key from
/.well-known/jwks.json. Cache this response — the key rotates infrequently. - Verify the RS256 signature. Use the public key from JWKS to verify the token's signature. Reject the token if verification fails.
- Check the issuer (
iss). Must match your UniAuth instance URL (e.g.,https://uniauth.id). - Check the audience (
aud). Must match your application's client ID. - Check the expiration (
exp). The current time must be before the expiration timestamp. Allow a small clock skew tolerance (30 seconds). - Verify the nonce. If you sent a
noncein the authorization request, verify that the same value appears in the ID token. This prevents replay attacks.
Node.js Example
import * as jose from "jose";
async function validateIdToken(idToken, clientId, issuer, expectedNonce) {
// Fetch JWKS from the discovery endpoint
const jwks = jose.createRemoteJWKSet(
new URL("/.well-known/jwks.json", issuer)
);
// Verify signature and decode
const { payload } = await jose.jwtVerify(idToken, jwks, {
issuer: issuer,
audience: clientId,
clockTolerance: 30, // 30 seconds
});
// Verify nonce (if sent during authorization)
if (expectedNonce && payload.nonce !== expectedNonce) {
throw new Error("Nonce mismatch — possible replay attack");
}
return payload;
}
// Usage
const claims = await validateIdToken(
idTokenFromResponse,
"your-client-id",
"https://uniauth.id",
storedNonce
);
console.log("User sub:", claims.sub);
console.log("Email:", claims.email);
console.log("Auth methods:", claims.amr);Python Example
import jwt
import requests
def validate_id_token(id_token, client_id, issuer, expected_nonce=None):
# Fetch JWKS
jwks_url = f"{issuer}/.well-known/jwks.json"
jwks = requests.get(jwks_url).json()
# Get the signing key
header = jwt.get_unverified_header(id_token)
key = next(k for k in jwks["keys"] if k["kid"] == header["kid"])
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(key)
# Verify and decode
payload = jwt.decode(
id_token,
public_key,
algorithms=["RS256"],
audience=client_id,
issuer=issuer,
leeway=30,
)
# Verify nonce
if expected_nonce and payload.get("nonce") != expected_nonce:
raise ValueError("Nonce mismatch")
return payload
# Usage
claims = validate_id_token(
id_token_from_response,
"your-client-id",
"https://uniauth.id",
stored_nonce,
)
print(f"User: {claims['sub']}, Email: {claims['email']}")Token Introspection
Resource servers can verify the validity of access tokens using the introspection endpoint. This is useful when you need to confirm a token is still active and retrieve its associated metadata server-side.
POST /api/oauth/introspect
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
token=ACCESS_TOKEN_TO_CHECK
HTTP/1.1 200 OK
Content-Type: application/json
{
"active": true,
"sub": "2b7f16373f76cbdf...",
"client_id": "your-client-id",
"scope": "openid profile email",
"token_type": "Bearer",
"exp": 1709049600,
"iat": 1709046000,
"iss": "https://uniauth.id"
}If the token is expired, revoked, or invalid, the response returns { "active": false }.
Token Revocation
Applications can revoke access tokens and refresh tokens when they are no longer needed (for example, when a user signs out of your application):
POST /api/oauth/revoke
Content-Type: application/x-www-form-urlencoded
token=TOKEN_TO_REVOKE
&client_id=your-client-id
&client_secret=your-client-secret
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true
}The revocation endpoint always returns a 200 response, even if the token was already expired or invalid. This is by design per RFC 7009 to prevent token probing.
Related documentation: OAuth2 / OIDC · Sessions · Security Guide · Error Reference