Token Introspection
Token introspection lets a resource server (an API that receives tokens) ask LumoAuth whether a token is still valid and retrieve metadata about it (client, user, scopes, expiration). Use it when you hold an opaque token, or when you need to know about revocation before the token expires. Terms are explained in the glossary.
POST /orgs/{orgId}/api/v1/oauth/introspect
When to Use Introspection
| Scenario | Recommendation |
|---|---|
| Opaque tokens | Required — opaque tokens can only be validated via introspection |
| JWT tokens (normal validation) | Not needed — validate the signature and expiration locally against the JWKS for better performance |
| JWT tokens (check for revocation) | Recommended for sensitive operations — local JWT validation cannot detect revocation |
| Getting token metadata | Use introspection when you need current scopes, user ID, or other claims |
Authentication
Call this endpoint from your backend using your client credentials (HTTP Basic or client_id/client_secret in the form body; discovery lists these as introspection_endpoint_auth_methods_supported). Never call it from client-side code. The calling client must be active and belong to the organization in the URL; a disabled client, a client from another organization, or a wrong secret all receive the same 401 invalid_client.
Only the client the token was issued to (or one named in its audience) can introspect it; other callers get {"active": false}.
Request
| Parameter | Required | Description |
|---|---|---|
token | Yes | The token to validate |
token_type_hint | No | access_token or refresh_token — speeds up lookup |
Example Request
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/introspect \
-u "CLIENT_ID:CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9..." \
-d "token_type_hint=access_token"
Response
Active Token
{
"active": true,
"scope": "openid profile email",
"client_id": "abc123def456",
"username": "john@example.com",
"token_type": "Bearer",
"exp": 1704067200,
"iat": 1704063600,
"sub": "12345",
"aud": "api.example.com",
"iss": "https://app.lumoauth.dev/orgs/acme-corp/api/v1"
}
iss is byte-identical to the discovery issuer of the entry point you called (https://auth.acme.com on a verified custom domain). If you compare it, compare it with that value.
Inactive / Invalid Token
If the token is expired, revoked, malformed, or does not exist, the response is simply the one below. The same applies to a still-unexpired user token whose user has since been blocked, deactivated or deleted, left the organization, signed out everywhere or had their password changed or reset, and to a token issued in another organization (including one for a CIMD client that uses the same client_id URL there). See Early invalidation.
{
"active": false
}
The server intentionally returns the same response for all invalid-token cases, so an attacker cannot distinguish between "unknown token" and "revoked token".
Response Fields
| Field | Type | Description |
|---|---|---|
active | boolean | Always present. Whether the token is currently valid. |
scope | string | Space-separated list of granted scopes |
client_id | string | Which client the token was issued to |
username | string | The user's email, for user-bound tokens |
token_type | string | Bearer for access tokens, refresh_token for refresh tokens |
sub | string | Subject — user ID or agent ID |
exp | integer | Expiration timestamp (Unix epoch seconds) |
iat | integer | Issued-at timestamp |
iss | string | Issuer, equal to the discovery issuer |
aud | string or array | Intended audience |
nbf | integer | Not-before timestamp, when the token carries one |
Token introspection is defined so resource servers that cannot (or should not) verify tokens locally can ask the authorization server. To comply, authenticate every introspection call, treat active: false as the only signal you need, and never expose the endpoint to browsers.
Troubleshooting
401 invalid_clientalthough the secret is right. The client was disabled in the portal, or you are calling another organization's endpoint.429 too_many_requests. The endpoint allows 100 calls per minute per client; cache results for the token's remaining lifetime instead of introspecting on every request.active: falsefor a token you just issued. You are introspecting with a client other than the one the token was issued to (and it is not in the token'saud).active: falsefor a token that has not expired. The user was blocked, deactivated or deleted, signed out everywhere, or changed or reset their password after the token was issued. The user must sign in again.issdoes not equal what your resource server expects. Read the issuer from discovery; it is.../orgs/{orgId}/api/v1on the organization path and the bare custom domain on a custom domain.
Related
- Token formats — local JWT validation
- Integrator checklist
Standards used on this page
| Plain name | Standard |
|---|---|
| Token introspection | RFC 7662 |
| JSON Web Key (JWK) and key sets (JWKS) | RFC 7517 |
See the standards reference for what each one covers and how LumoAuth uses it.