Token Management
After a successful authorization code exchange, you receive three tokens. This guide covers how to verify, refresh, revoke, and introspect them.
Token types and lifetimes
| Token | Format | Lifetime | Purpose |
|---|---|---|---|
| Access token | RS256 JWT | 1 hour | Call the UserInfo endpoint and other Xident APIs |
| Refresh token | Opaque string (xrt_ prefix) | 30 days (rotates on use) | Obtain new access tokens without re-authentication |
| ID token | RS256 JWT | 1 hour | Contains user claims (age, profile, etc.) for your app |
Access token (RS256 JWT)
The access token is a signed JWT that you use as a Bearer token when calling Xident APIs. You can decode it to read the payload, but you must verify the signature to ensure it was not tampered with.
Token structure
// Access token is an RS256 JWT
// Decode it to inspect (but always verify the signature!)
//
// Header:
{
"alg": "RS256",
"typ": "JWT",
"kid": "xident-2026-02"
}
// Payload:
{
"iss": "https://api.xident.io",
"sub": "xid_abc123def456",
"aud": "xc_your_client_id",
"exp": 1709312400,
"iat": 1709308800,
"scope": "openid age_verification profile",
"client_id": "xc_your_client_id"
} Verifying the signature
Fetch Xident's public keys from the JWKS endpoint and verify the JWT signature. Cache the keys — they rotate infrequently.
// Verify the access token signature (Node.js)
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
const client = jwksClient({
jwksUri: 'https://api.xident.io/.well-known/jwks.json',
cache: true, // Cache the signing keys
rateLimit: true, // Prevent excessive requests
cacheMaxAge: 86400000, // Cache for 24 hours
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) return callback(err);
callback(null, key.getPublicKey());
});
}
function verifyAccessToken(token) {
return new Promise((resolve, reject) => {
jwt.verify(token, getKey, {
issuer: 'https://api.xident.io',
audience: process.env.XIDENT_CLIENT_ID,
algorithms: ['RS256'],
}, (err, decoded) => {
if (err) return reject(err);
resolve(decoded);
});
});
}
// Usage
try {
const payload = await verifyAccessToken(accessToken);
console.log('User ID:', payload.sub);
console.log('Scopes:', payload.scope);
} catch (err) {
console.error('Invalid token:', err.message);
} Verification checklist
- Signature is valid (RS256 with Xident's public key)
issishttps://api.xident.ioaudmatches yourclient_idexphas not passediatis not in the future
ID token
The ID token is also an RS256 JWT, but it embeds the user's claims directly in the payload. You can decode and verify it the same way as the access token. It saves you a round-trip to the UserInfo endpoint when you only need the claims that were available at authorization time.
// ID token payload (RS256 JWT)
{
"iss": "https://api.xident.io",
"sub": "xid_abc123def456",
"aud": "xc_your_client_id",
"exp": 1709312400,
"iat": 1709308800,
"auth_time": 1709308790,
"nonce": "your_nonce_value",
// Claims from requested scopes are embedded in the ID token:
"age_verified": true,
"age_bracket": "18+",
"age_brackets_verified": ["12+", "15+", "18+"],
"verification_level": "ml",
"verified_at": "2026-02-15T10:30:00Z",
"preferred_username": "alex_t",
"display_name": "Alex T.",
"email": "alex@example.com",
"email_verified": true
} Refresh token rotation
Xident uses refresh token rotation. Every time you use a refresh token, you receive a new refresh token and the old one is invalidated. This limits the window of exposure if a refresh token is compromised.
Refreshing tokens
// Refresh an expired access token
const response = await fetch('https://api.xident.io/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: process.env.XIDENT_CLIENT_ID,
client_secret: process.env.XIDENT_CLIENT_SECRET,
refresh_token: storedRefreshToken,
}),
});
const tokens = await response.json();
// {
// "access_token": "eyJhbG...", // New access token
// "token_type": "Bearer",
// "expires_in": 3600,
// "refresh_token": "xrt_new...", // NEW refresh token (rotation!)
// "id_token": "eyJhbG...", // New ID token
// "scope": "openid age_verification"
// }
// IMPORTANT: Replace the old refresh token with the new one!
// The old refresh token is now invalid.
await saveRefreshToken(tokens.refresh_token); Never reuse old refresh tokens
If Xident detects a previously used refresh token being reused, it treats this as a potential replay attack and revokes the entire token family — the user will need to log in again. Always store and use only the latest refresh token.
Safe rotation pattern
// Safe refresh token rotation pattern
async function refreshAccessToken(currentRefreshToken) {
try {
const response = await fetch('https://api.xident.io/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: process.env.XIDENT_CLIENT_ID,
client_secret: process.env.XIDENT_CLIENT_SECRET,
refresh_token: currentRefreshToken,
}),
});
if (!response.ok) {
if (response.status === 401) {
// Refresh token was revoked or expired — user must re-authenticate
await clearUserSession();
throw new Error('Session expired. Please log in again.');
}
throw new Error('Token refresh failed');
}
const tokens = await response.json();
// CRITICAL: Atomically replace the refresh token
// If you store the old one and use it again, it will be rejected
// and the entire token family will be revoked (replay detection)
await atomicTokenUpdate({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt: Date.now() + tokens.expires_in * 1000,
});
return tokens.access_token;
} catch (error) {
console.error('Refresh failed:', error);
throw error;
}
} Revoking tokens
Revoke tokens when a user logs out, disconnects your app, or when you detect suspicious activity. Revoking a refresh token also invalidates all access tokens in the same family.
// Revoke a token (access or refresh)
const response = await fetch('https://api.xident.io/oauth/revoke', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: process.env.XIDENT_CLIENT_ID,
client_secret: process.env.XIDENT_CLIENT_SECRET,
token: refreshToken,
token_type_hint: 'refresh_token', // or 'access_token'
}),
});
// Returns 200 OK regardless of whether the token existed
// (to prevent token existence enumeration) Introspecting tokens
Use token introspection to check if a token is still valid without decoding it yourself. This is useful for opaque tokens (refresh tokens) or when you want server-side validation.
// Introspect a token to check if it's still valid
const response = await fetch('https://api.xident.io/oauth/introspect', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: process.env.XIDENT_CLIENT_ID,
client_secret: process.env.XIDENT_CLIENT_SECRET,
token: accessToken,
token_type_hint: 'access_token',
}),
});
const result = await response.json();
// Active token:
// {
// "active": true,
// "sub": "xid_abc123def456",
// "client_id": "xc_your_client_id",
// "scope": "openid age_verification",
// "exp": 1709312400,
// "iat": 1709308800,
// "token_type": "Bearer"
// }
//
// Expired/revoked token:
// { "active": false } Best practices
- Store refresh tokens securely — Encrypt at rest, never expose to the frontend
- Refresh proactively — Refresh the access token before it expires (e.g., when 5 minutes remain)
- Handle rotation atomically — Update the stored refresh token in a single transaction to avoid race conditions
- Revoke on logout — Always revoke the refresh token when the user logs out
- Cache JWKS keys — Fetch Xident's public keys once and cache them (they rotate monthly)
- Use the ID token for initial claims — Avoid an extra API call by reading claims from the ID token
- Verify every token — Always validate signature, issuer, audience, and expiration
Next steps
- Security Best Practices — Full security guide for your OAuth integration
- API Reference — Complete endpoint documentation
- Scopes & Claims — All available scopes and what they return