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:

TokenFormatAlgorithmLifetimeRecommended Storage
Session TokenJWTHS2567 dayshttpOnly cookie (managed by UniAuth)
Access TokenJWTHS2561 hourIn-memory (client application)
ID TokenJWTRS2561 hourIn-memory (client application)
Refresh TokenOpaque stringN/A30 daysSecure server-side storage
Authorization CodeOpaque stringN/A10 minutesOne-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

ClaimTypeDescription
idstring (UUID)The user's unique identifier
emailstringThe user's email address
first_namestringFirst name (may be null)
last_namestringLast name (may be null)
avatar_urlstringURL to the user's avatar image (may be null)
rolestringThe user's role (e.g., "user", "admin", "moderator")
amrstring[]Authentication Methods Reference — list of methods used during login
auth_timenumberUnix 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:

ValueMeaning
pwdPassword-based authentication
mfaMulti-factor authentication was completed
otpTime-based one-time password (authenticator app)
smsSMS verification code
hwkHardware 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

ClaimTypeDescription
substringApp-specific pairwise subject identifier (not the user's real ID)
audstringThe client ID of the application this token was issued to
scopestringSpace-separated list of granted scopes
jtistringUnique token identifier
issstringIssuer URL (your UniAuth instance)
iatnumberUnix timestamp when the token was issued
expnumberUnix 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:

ScopeClaims Included
openidsub, aud, iss, auth_time, amr, sid, at_hash, nonce
profilename, given_name, family_name, preferred_username, picture, profile, website, gender, birthdate, locale, zoneinfo, updated_at
emailemail, email_verified
phonephone_number, phone_number_verified
addressaddress (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_challenge parameter using the S256 method. The corresponding code_verifier must 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=codeVerifier

Pairwise 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:

  1. Fetch the JWKS. Retrieve the public signing key from /.well-known/jwks.json. Cache this response — the key rotates infrequently.
  2. Verify the RS256 signature. Use the public key from JWKS to verify the token's signature. Reject the token if verification fails.
  3. Check the issuer (iss). Must match your UniAuth instance URL (e.g., https://uniauth.id).
  4. Check the audience (aud). Must match your application's client ID.
  5. Check the expiration (exp). The current time must be before the expiration timestamp. Allow a small clock skew tolerance (30 seconds).
  6. Verify the nonce. If you sent a nonce in 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