Validate tokens in your API
Verify Obexal access tokens on your resource server: local JWKS validation versus RFC 7662 introspection, revocation, lifetimes and refresh token rotation.
Your API receives access tokens issued by Obexal and must decide, on every request, whether to trust them. There are two ways to do that: validate the JWT locally against the public keys, or ask Obexal through the introspection endpoint. This page covers both, plus revocation and refresh token hygiene.
Access tokens are JWTs signed with RS256 (header typ: at+jwt). Refresh tokens are opaque strings: they cannot be validated locally, only introspected or exchanged.
Examples use accounts.obexal.com, the default domain. With a custom domain, that domain is the issuer.
Local validation with JWKS
Any standard JWT library can validate an Obexal access token:
- Fetch the JWKS from
https://accounts.obexal.com/.well-known/jwks.jsonand cache it (the response is served with a short public cache). It contains the active key and retired keys that have not yet expired, so rotation is transparent. - Select the key by the
kidheader and accept RS256 only: rejectnoneand any HS* algorithm (algorithm confusion defense). - Verify the signature, the
issclaim (your issuer URL) andexp. - Check that the JWT header
typisat+jwt: this rejects an ID token replayed as an access token. - Enforce your own authorization:
scope, andaudwhere relevant (delegated agent tokens can be bound to a specific resource server viaaud).
Introspection (RFC 7662)
POST /oauth/introspect returns the live state of a token. The endpoint is protected: only an authenticated confidential client may call it (a public client receives 401 invalid_client), and a client can only introspect its own tokens. Any other token, and any unknown, expired or revoked token, answers {"active": false} with no further detail. Both access tokens (JWT) and refresh tokens (opaque) are accepted.
curl -sS -X POST https://accounts.obexal.com/oauth/introspect \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d "token=$ACCESS_TOKEN"{
"active": true,
"token_type": "access_token",
"client_id": "app_a1b2c3",
"scope": "openid profile email",
"sub": "8f2c1e9a-4b7d-4c2e-9f1a-3d5e7b9c0a12",
"username": "alice@example.eu",
"iss": "https://accounts.obexal.com",
"aud": "app_a1b2c3",
"exp": 1751450000,
"iat": 1751449400,
"act": { "sub": "agent_7d4e" }
}The act member only appears on delegated tokens: it identifies the AI agent acting on behalf of the user in sub, chained for nested delegation. See Delegation with Token Exchange.
Choosing between the two
- Local JWKS validation costs no network call per request and scales with your API. Its limit: a token stays valid until
exp, so revocation is only as fast as the token lifetime (10 minutes by default). - Introspection returns the live truth: it reflects revocations immediately, including the kill switch of a disabled AI agent, whose tokens report
active: falseeven before they expire. It is also the only option for refresh tokens. Its cost: one HTTP call and a client secret on your resource server.
A common pattern: validate locally on the hot path, and introspect for sensitive operations where immediate revocation matters.
Lifetimes and revocation
| Token | Default lifetime | Form |
|---|---|---|
| Access token | 10 minutes | JWT RS256, typ: at+jwt |
| ID token | 10 minutes | JWT RS256 |
| Refresh token | 30 days | Opaque, 256 bits, hash stored server-side |
On a self-hosted deployment these are configurable; see Configuration.
POST /oauth/revoke (RFC 7009) revokes a refresh token. The call is client-authenticated and always returns 200, even for an unknown or already revoked token (idempotent by design):
curl -sS -X POST https://accounts.obexal.com/oauth/revoke \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d "token=$REFRESH_TOKEN" -d 'token_type_hint=refresh_token'
# 200, alwaysAccess tokens are not individually revocable: they simply expire, which is why their lifetime is short. When you need instant invalidation on top of that, use introspection.
Refresh tokens: rotation and replay detection
Refresh tokens rotate on every use: the token you present is revoked and a new one is issued in the same chain, with the same scopes or a requested subset. Store the new token and discard the old one.
Presenting an already revoked refresh token is treated as a compromise signal, not a retry: Obexal revokes the entire chain for that user and client, refuses the request with invalid_grant, and records a critical security incident (refresh_token_replay) surfaced in the admin console.
Treat invalid_grant on a refresh call as a re-authentication event for the user. Never retry the old token in a loop.