Skip to content

Introspection & Revocation

While JWT access tokens can be validated locally using public keys, some scenarios require communicating with the authorization server:

  • Token Introspection (RFC 7662) — Ask the authorization server whether a token is currently active
  • Token Revocation (RFC 7009) — Tell the authorization server to invalidate a token

Both features require clientCredentials (a confidential client) because the authorization server authenticates the caller.

To use introspection or revocation, configure the endpoints and client credentials:

import { createAuthrimServer } from '@authrim/server';
const authrim = createAuthrimServer({
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
// Introspection endpoint
introspectionEndpoint: 'https://auth.example.com/oauth/introspect',
// Revocation endpoint
revocationEndpoint: 'https://auth.example.com/oauth/revoke',
// Required for both introspection and revocation
clientCredentials: {
clientId: 'my-resource-server',
clientSecret: 'resource-server-secret',
},
});
await authrim.init();

The SDK uses HTTP Basic authentication with the client credentials when calling these endpoints.


Token introspection allows you to query the authorization server for the current status of a token. This is especially useful for:

  • Opaque tokens — Tokens that are not self-contained JWTs and cannot be validated locally
  • Real-time validity checks — Detecting revoked or expired tokens immediately
  • Token metadata — Retrieving claims that may not be in the JWT
const response = await authrim.introspect(token, tokenTypeHint?, issuerOptions?);
ParameterTypeDescription
tokenstringThe access token to introspect
tokenTypeHint'access_token' | 'refresh_token' | 'device_secret'Optional token type hint
issuerOptions{ issuer?: string; token?: ValidatedToken }Optional issuer selector for multi-issuer deployments
interface IntrospectionResponse {
active: boolean;
sub?: string;
scope?: string;
client_id?: string;
username?: string;
token_type?: string;
exp?: number;
iat?: number;
nbf?: number;
aud?: string | string[];
iss?: string;
jti?: string;
cnf?: { jkt?: string };
installation_id?: string;
app_display_name?: string;
platform?: string;
display_name?: string;
fallback_display_name?: string;
[key: string]: unknown; // Additional claims
}

The most important field is active:

  • active: true — The token is valid and currently active
  • active: false — The token is invalid, expired, revoked, or unknown
const response = await authrim.introspect(accessToken);
if (!response.active) {
// Token is not valid — reject the request
return res.status(401).json({ error: 'invalid_token' });
}
// Token is active — use the claims
console.log('Subject:', response.sub);
console.log('Scopes:', response.scope);
console.log('Client:', response.client_id);

The introspection response active: false covers multiple scenarios:

Scenarioactive valueNotes
Valid tokentrueToken is current and usable
Expired tokenfalseToken’s exp has passed
Revoked tokenfalseToken was explicitly revoked
Unknown tokenfalseToken was not issued by this server
Malformed tokenfalseToken cannot be parsed
const response = await authrim.introspect(accessToken);
if (!response.active) {
return res.status(401).json({ error: 'invalid_token' });
}
// Check scopes manually for introspected tokens
const grantedScopes = (response.scope ?? '').split(' ');
const requiredScopes = ['read:orders', 'write:orders'];
const hasAllScopes = requiredScopes.every(
(scope) => grantedScopes.includes(scope)
);
if (!hasAllScopes) {
return res.status(403).json({ error: 'insufficient_scope' });
}

Token revocation tells the authorization server to invalidate a specific token. After revocation, the token will no longer be accepted by the authorization server or by resource servers that use introspection.

await authrim.revoke(token, tokenTypeHint?, issuerOptions?);
ParameterTypeDescription
tokenstringThe token to revoke
tokenTypeHint'access_token' | 'refresh_token' | 'device_secret'Optional token type hint
issuerOptions{ issuer?: string; token?: ValidatedToken }Optional issuer selector for multi-issuer deployments
try {
await authrim.revoke(accessToken, 'access_token');
console.log('Token revoked successfully');
} catch (error) {
console.error('Revocation failed:', error.message);
}

Native SSO device_secret values use the same introspection and revocation endpoints with the device_secret token type hint. Prefer the dedicated helpers so the hint is always correct:

const response = await authrim.introspectDeviceSecret(deviceSecret, {
issuer: 'https://auth.example.com',
});
if (response.active) {
console.log(response.installation_id);
console.log(response.platform);
}
await authrim.revokeDeviceSecret(deviceSecret, {
issuer: 'https://auth.example.com',
});

Avoid logging raw device_secret values. Treat them like refresh-token grade credentials.

  • Logout — Revoke the user’s access token when they sign out
  • Security incident — Immediately invalidate a compromised token
  • Permission change — Revoke tokens when a user’s permissions are updated (force re-authentication)
// Example: Revoke on logout
app.post('/api/logout', auth(), async (req, res) => {
const token = req.authrim!.token.token;
// Revoke the access token at the authorization server
await authrim.revoke(token, 'access_token');
res.json({ message: 'Logged out' });
});

Choosing between local JWT validation and token introspection depends on your requirements:

CriterionLocal ValidationIntrospection
Token formatJWT onlyJWT or opaque
Network callNo (uses cached JWKS)Yes (every call)
LatencyLocal crypto and cache lookupAuthorization-server round trip
Detects revocationNoYes
Real-time validityNo (relies on exp)Yes
Requires client credentialsNoYes
Works offlineYes (after JWKS cached)No
flowchart TD
    A["Token received"] --> B{"Token format?"}
    B -->|JWT| C{"Need real-time<br>revocation check?"}
    B -->|Opaque| D["Use Introspection"]
    C -->|Yes| E["Use Introspection"]
    C -->|No| F{"Sensitive<br>operation?"}
    F -->|Yes| G["Use Both:<br>Local + Introspection"]
    F -->|No| H["Use Local Validation"]

For sensitive operations, combine local validation with introspection:

// Fast path: Local JWT validation
const tokenResult = await authrim.validateToken(accessToken);
// For sensitive operations, also check with the authorization server
if (isSensitiveOperation(req.path)) {
const introspectionResult = await authrim.introspect(accessToken);
if (!introspectionResult.active) {
return res.status(401).json({
error: 'invalid_token',
error_description: 'Token has been revoked',
});
}
}

This gives you the best of both worlds:

  • Fast validation for most requests (local JWT verification)
  • Real-time validity for critical operations (introspection)

For high-throughput APIs, you can cache introspection results for a short period:

const INTROSPECTION_CACHE_TTL = 30_000; // 30 seconds
async function introspectWithCache(token: string) {
const cacheKey = `introspect:${hashToken(token)}`;
// Check cache first
const cached = await cache.get(cacheKey);
if (cached) return cached;
// Call authorization server
const result = await authrim.introspect(token);
// Cache the result (short TTL)
if (result.active) {
await cache.set(cacheKey, result, INTROSPECTION_CACHE_TTL);
}
return result;
}
ErrorDescription
IntrospectionErrorFailed to call the introspection endpoint
RevocationErrorFailed to call the revocation endpoint
ClientCredentialsErrorMissing or invalid client credentials
NetworkErrorNetwork connectivity issue