AUTH4D-155 Token introspection
Access tokens are signed and self-contained, so a server that verifies them offline keeps accepting
one for up to five minutes after its session is revoked (AUTH4D-25 (repository file docs/api-spec/auth4d-25.md)). Token
introspection (RFC 7662) lets a confidential server ask
the auth origin whether the session behind an access token is still live, so revocation takes effect
at once for that server.
Endpoint
POST /oauth/introspect?tenant_id={tenantId} with an application/x-www-form-urlencoded body. The
tenant may also be sent as the tenant_id form field. Tenant discovery advertises the endpoint:
{
"introspection_endpoint": "https://auth.auth4.dev/oauth/introspect?tenant_id={tenantId}",
"introspection_endpoint_auth_methods_supported": ["client_secret_basic"]
}
Both discovery fields are optional in OidcDiscoveryDocumentSchema, so documents from older
deployments still parse.
| Field | Required | Notes |
|---|---|---|
token |
Yes | The access token to check. At most 16,384 characters. |
token_type_hint |
No | Accepted and ignored. Only access tokens can be introspected. |
tenant_id |
No | Alternative to the query parameter. |
Client authentication
The caller must be an enabled confidential client (clientType: "server",
tokenEndpointAuthMethod: "client_secret_basic") of the same active customer tenant, and must
authenticate with HTTP Basic. The client ID and secret are form-encoded before Basic encoding, as in
RFC 6749 section 2.3.1.
Everything else gets 401 {"error":"invalid_client"} with
WWW-Authenticate: Basic realm="auth4", charset="UTF-8", before the token is looked at:
- no credentials, or a
client_idin the body without Basic credentials; - a public browser or native client, with or without an invented secret;
- a wrong secret, a secret sent in the body (
client_secret), or different client IDs in the body and in Basic credentials; - a disabled client, a client of another tenant, an unknown or suspended tenant.
The control-plane realm is never served: its ID is not a customer tenant ID, so the request gets
400 invalid_request.
Project owners and admins create server clients, and rotate or delete their secrets, through the
management API and the console (AUTH4D-207 (repository file docs/api-spec/auth4d-207.md)).
Browsers
Introspection is for servers. A request with any Origin header, including the console and auth
origins, gets 403 invalid_request without CORS headers and without checking credentials. Origins
that are not on the deployment-wide list are refused at the runtime boundary with 403.
Rate limit
Each confidential client may make 600 introspection requests per minute. The bucket is kept in the
tenant’s rate-limit Durable Object under a privacy-hashed key. Over the limit the response is 429
{"error":"slow_down"} with Retry-After in seconds. A short client-side cache, such as the server
SDK’s, keeps a busy server well below this.
Response
200 with Cache-Control: no-store for every authenticated request with a token.
An active token:
{
"active": true,
"scope": "openid email",
"client_id": "cli_…",
"sub": "usr_…",
"aud": "cli_…",
"iss": "https://auth.auth4.dev/t/ten_…",
"exp": 1791037100,
"iat": 1791036800,
"jti": "…",
"token_type": "Bearer",
"tenant_id": "ten_…",
"sid": "ses_…",
"auth4_guest": true
}
sid is present when the token is bound to a session, and auth4_guest when the token carries the
guest claim (repository file docs/api-spec/auth4d-64.md). The response never includes profile data such as an email address.
A token is reported active only when all of these hold:
- it is an access token (
token_use: "access") signed by a current tenant signing key; - its issuer is this tenant’s issuer, its
tenant_idis this tenant, and its audience is the client named in itsclient_id; - it has not expired;
- its session is live in the session-state Durable Object, the authority for revocation;
- the token’s application is still enabled and the player is not disabled.
Anything else is {"active": false} with no other fields: revoked or expired sessions, tokens of
another tenant, ID tokens, refresh tokens, and malformed or tampered values. RFC 7662 allows a
server to answer active: false for token types it does not introspect.
If the session authority cannot be reached, the response is 503 temporarily_unavailable rather
than an answer, so an outage is never reported as either active or revoked.
Which tokens a client may check
Any confidential client of a tenant may introspect any access token of that tenant, whichever
application it was issued to. The response names the token’s own client_id, not the caller. This
matches offline verification, which already lets the tenant’s servers read the same claims from the
signed token; introspection only adds whether the session is still live. Tokens of other tenants are
always active: false.
Server SDK introspect mode
@auth4/sdk-server gains an opt-in introspection option:
const auth4 = new Auth4ServerClient({
tenantId,
issuer,
audience,
introspection: { clientId, clientSecret, cacheTtlMs: 5_000 },
});
- The token is verified offline first, exactly as before. Only a token that passes is introspected.
- The endpoint is derived from the configured issuer’s origin (
/oauth/introspect?tenant_id=…) and exposed asintrospectionUrl. It is never read from a token or from discovery. - An active answer is reused for
cacheTtlMs(default 5,000, at most 60,000,0disables reuse) and never past the token’s expiry. An inactive answer is kept until the token expires, because a revoked session does not come back. Concurrent checks of one token share one request. The cache is keyed by a SHA-256 of the token and holds at most 10,000 entries. - An active answer must name the verified token’s
sub,client_id,jtiand the configured tenant; otherwise the token is rejected withinvalid_token. - An inactive answer rejects the token with
invalid_token. A non-200 response, a network error, a timeout (introspectionTimeoutMs, default 3,000) or a body without a booleanactiverejects it with the new error codeintrospection_unavailable. The SDK fails closed. - The client secret is kept in a private field.
optionsomits theintrospectionsettings, so the secret never appears inoptionsor inJSON.stringify(client).
Without the option, behavior is unchanged.
Tests
tests/integration/auth4d-155.introspection.test.ts runs the unmodified API composition with real
D1, session Durable Objects and rate-limit state. It covers discovery, active and revoked tokens,
disabled players and applications, inactive token types, request validation, client authentication,
disabled clients, tenant isolation, the tenant-wide client rule, browser refusal, the rate limit, and
the SDK’s revocation, caching, credential handling and fail-closed behavior.
Source: docs/api-spec/auth4d-155.md