Choosing between JWT and opaque access tokens decides where trust lives: in a signing key every API must handle correctly, or in an authorisation server every API must call. The choice then sets how fast revocation takes effect, how much personal data leaks to clients and logs, and what an attacker gains by stealing a signing key rather than a single token. This guide compares self-contained JWT access tokens (RFC 9068) with opaque reference tokens validated by introspection (RFC 7662), gives a short answer, a side-by-side table, decision guidance by API type and the hybrid patterns most large estates end up with. It is for API owners, gateway and platform engineers and identity architects. JWT construction and validation detail lives in the JWT Security Guide; endpoint mechanics live in the OAuth Token Introspection and Revocation Guide.
Key takeaways
- The difference is by value versus by reference. A JWT carries its claims, signed, and the API validates it locally. An opaque token is a random handle; the API asks the authorisation server what it means.
- JWTs trade revocation for speed. A locally validated JWT stays usable until
expunless the API checks a revocation list. Opaque tokens can die the moment the server marks them inactive, subject only to introspection caching. - JWTs expose their contents. RFC 9068 notes that clients and possibly end users can read unencrypted claims, and says the client MUST NOT inspect the access token. Opaque tokens reveal nothing to anyone holding them.
- Each fails differently. JWT risk concentrates in signing keys and validation code (algorithm, issuer, audience, type). Opaque risk concentrates in the availability and access control of the introspection endpoint.
- Hybrids are common and sensible. Opaque tokens outside the perimeter, exchanged for short JWTs inside it, give clients nothing to read and internal APIs nothing to call.
The short answer
Use JWT access tokens when many internal APIs need to validate at high volume with low latency, lifetimes are short (minutes), and the claims carry nothing sensitive. Use opaque tokens when tokens leave your control (third-party clients, browsers, mobile apps), when you need revocation to bite immediately, or when the authorisation data includes personal or sensitive attributes. Where both conditions hold, issue opaque tokens to clients and translate them at the edge.
Neither format changes the bearer problem. Both are bearer tokens unless bound with DPoP (RFC 9449) or mTLS (RFC 8705), and both need TLS, audience restriction and short lifetimes. The format choice is about validation, revocation and disclosure, not theft resistance.
How each is validated
JWT access tokens (RFC 9068)
RFC 9068 profiles JWT access tokens. They must be signed, must not use alg none, and must carry typ at+jwt. Required claims are iss, exp, aud, sub, client_id, iat and jti. Section 4 tells the resource server to reject any other typ, decrypt if encrypted, match iss exactly, confirm aud contains its own identifier, verify the signature with the authorisation server's keys and reject expired tokens, returning invalid_token on failure. Keys come from the issuer's JWKS, cached and refreshed on rotation. See the JSON Web Key Sets (JWKS) and Signing Key Rotation Guide.
RFC 8725 (JSON Web Token Best Current Practices, BCP 225) adds the rules that most JWT incidents trace back to: libraries MUST let the caller specify allowed algorithms, each key is used with one algorithm, keys must be verified as belonging to the issuer, aud must be checked and the token rejected if it is missing or wrong, kid, jku and x5u are untrusted input, and explicit typing with mutually exclusive validation rules stops one kind of JWT being accepted as another.
Opaque tokens and introspection (RFC 7662)
An opaque token is a high-entropy string with no meaning outside the authorisation server. The resource server calls the introspection endpoint with the token and receives JSON whose only required member is active; optional members include scope, client_id, username, token_type, exp, iat, nbf, sub, aud, iss and jti. RFC 7662 section 2.1 says the server MUST require some form of authorisation to call the endpoint, so attackers cannot use it to test stolen or guessed tokens, and for an inactive token the server should return only active: false.
RFC 9701 (January 2025, Standards Track) lets a resource server request the introspection response as a signed, optionally encrypted JWT with Accept: application/token-introspection+jwt, for cases that need stronger assurance that the authorisation server stands behind the response.
Side-by-side comparison
| Property | JWT access token | Opaque token with introspection |
|---|---|---|
| Standard | RFC 7519, RFC 9068 profile, RFC 8725 BCP | RFC 6749 (format left to the server), RFC 7662, RFC 9701 |
| What the token holds | Signed claims: issuer, subject, audience, client, scope, expiry | A random reference; claims stay on the server |
| Validation at the API | Local: signature, typ, iss, aud, exp, scope | Network call to the introspection endpoint, then local checks on the response |
| Latency and load | No per-request call; JWKS fetched occasionally | One call per token per cache window; authorisation server on the hot path |
| Revocation | Effective at exp unless the API checks a deny list or introspects | Effective at the next introspection; delay equals cache lifetime (RFC 7662 section 4) |
| Changes to permissions | Claims frozen at issuance until exp | Reflected at the next introspection |
| Readable by the client or user | Yes, unless encrypted (JWE) | No |
| Privacy at resource servers | Every API in aud sees every claim | The server can tailor or minimise the response per caller |
| Main failure mode | Signing key compromise or weak validation code: forged tokens accepted | Introspection outage or unauthenticated endpoint: APIs fail closed or tokens can be probed |
| Size | Hundreds of bytes to kilobytes; can hit header limits | Small, fixed length |
| Offline or cross-domain validation | Yes, with the issuer's public keys | Only where the API can reach the issuer |
Revocation: the core trade-off
With JWTs, the cheapest revocation is a short exp. Everything else adds state back into a stateless design:
- Short lifetimes: 5 to 15 minutes bounds the window after a grant is revoked. Pair them with refresh tokens held at the authorisation server, which can be revoked at once under RFC 7009. See the OAuth Refresh Token Rotation and Expiry Guide.
- Deny lists by
jtiorsid: APIs check a shared, short-lived cache of revoked identifiers. Entries only need to live until the token'sexp. - Event-driven invalidation: session and credential change events pushed to APIs and gateways, for example through the Shared Signals Framework and CAEP. See the CAEP and Continuous Access Evaluation Guide.
- Introspect for high-risk calls only: validate locally for reads, introspect before payments, deletions or privilege changes.
With opaque tokens, revocation is native but caching reintroduces delay. RFC 7662 is explicit that a cached response can let a revoked token through for the cache period, that highly sensitive systems may disable caching, and that a response must not be cached beyond the token's exp. A 30 to 60 second cache is a common compromise for general APIs; zero for administrative ones. OWASP ASVS 5.0 V8.3.2 frames the same issue from the authorisation side: changes to values on which authorisation decisions are made must apply immediately, and where self-contained tokens prevent that, mitigating controls are required.
Privacy and disclosure
- Clients can read JWTs: RFC 9068 section 6 notes that clients, and possibly end users, can read unencrypted claims, and offers three mitigations: encrypt the token or sensitive claims, omit sensitive claims, or use opaque tokens.
- Clients start depending on claims: because the client MUST NOT inspect the access token under RFC 9068, any client logic built on reading it is a defect that breaks when the format changes. Opaque tokens make the mistake impossible.
- Every API sees every claim: a JWT sent to a resource server reveals all of its claims to that server. RFC 9068 asks the authorisation server to confirm the resource server is entitled to the information, and to choose
subvalues after a privacy impact assessment, for example pairwise identifiers per resource server to prevent tracking. - Introspection has its own exposure: RFC 7662 section 5 warns that responses may contain privacy-sensitive identifiers and suggests service-specific identifiers or omitting fields. RFC 9701 adds that introspection requests can reveal when a user accesses a resource server.
- Logs: JWTs in logs are readable data, not just credentials. Redact both formats. See the JWT and Session Tokens in Code and Logs Guide.
When to use which: decision guidance
| Scenario | Recommended format | Why |
|---|---|---|
| Internal microservices, high call volume, same trust domain | Short-lived JWT (RFC 9068) | No per-call dependency on the authorisation server; claims stay internal |
| Public API used by third-party clients | Opaque, introspected at the gateway | Clients cannot read or depend on claims; revocation takes effect quickly |
| Browser or mobile app calling your APIs | Opaque to the client, or tokens held by a backend for frontend | Nothing readable on the device |
| Administrative, financial or bulk-export APIs | Opaque with no or very short cache, or JWT plus introspection on sensitive actions | Revocation and permission changes must apply at once |
| Claims include personal or sensitive attributes | Opaque, or encrypted JWT | Avoid disclosure to clients and every API in the audience |
| Validation across organisations or offline | JWT with published JWKS | The API may not be able to reach the issuer |
| AI agents and machine clients calling many tools | Short-lived, audience-restricted JWT per tool, issued by exchange | Each tool validates locally; narrow audience limits replay |
Small organisations: use what your identity provider issues by default, keep JWT lifetimes short, and use a well-maintained validation library with an explicit algorithm allowlist rather than writing checks yourself. Large organisations: standardise the hybrid pattern at the API gateway so individual teams never parse external tokens, and publish one validation profile (algorithms, issuers, audiences, maximum lifetime, clock skew) for internal JWTs.
How they work together: hybrid patterns
- Opaque outside, JWT inside: clients hold opaque tokens; the gateway introspects once, caches briefly and forwards a short-lived internal JWT to back-end services. This pattern is sometimes called a phantom or split token. Clients see nothing, services validate locally.
- Token exchange at the boundary: the gateway or service uses OAuth 2.0 Token Exchange (RFC 8693) to swap the incoming token for one with a narrower audience and scope for the next hop. See the OAuth Token Exchange and Delegation Guide and the OAuth Transaction Tokens (Txn-Tokens) Guide.
- JWT with selective introspection: APIs validate JWTs locally and introspect (same token, if the server supports introspecting its own JWTs) before high-risk operations.
- Sender constraint on either format: DPoP and mTLS add a
cnfbinding to JWTs or to the introspection response, so a stolen token of either type is useless without the key. See the DPoP Guide.
Implementing and switching formats
- Write the validation profile first: allowed
algvalues, issuer, audiences per API,typat+jwt, maximum lifetime, clock skew, and introspection cache lifetime per API class. - Centralise validation: put it in the gateway or a shared library so every API applies the same checks. Test negative cases: wrong
alg,algnone, wrongaud, ID token presented as access token, expired token, unknownkid. - Pilot per audience: change the token format for one resource server's audience at the authorisation server, keep the old validator accepting both during the window, and watch
invalid_tokenrates. - Protect the introspection endpoint: require client authentication from resource servers, rate-limit it, and size it for peak load because it is now on the critical path.
- Plan for outages: decide per API whether to fail closed when introspection is unavailable (sensitive APIs) or to accept still-valid cached results for a bounded time (general APIs).
- Roll back: token format is set per audience at the authorisation server, so reverting is a configuration change; keep dual validation until clients have cycled through at least one maximum token lifetime.
Detection and measurement
- JWT validation failures by reason: bad signature, unknown
kid, wrongissoraud, wrongtyp. A spike in signature orkidfailures can mean forging attempts. - Tokens valid by signature but unknown to the issuer: a correctly signed JWT whose
jtinever appears in the authorisation server's issuance log indicates a compromised signing key. - Introspection anomalies: high rates of
active: falsefrom one caller, calls from unregistered resource servers, and failed client authentication at the endpoint. - Use after revocation: requests carrying a
jtiorsidon the deny list.
| Metric | What good looks like |
|---|---|
| Maximum JWT access token lifetime | 15 minutes or less for internal APIs |
APIs validating aud, iss, typ and an algorithm allowlist | 100%, checked by negative tests in CI |
| Introspection cache lifetime on administrative APIs | Zero or a few seconds |
| Time from grant revocation to last accepted request | Within the JWT lifetime or cache window, measured in exercises |
| Signed JWTs not matched to an issuance record | Zero |
| Introspection endpoint availability | Matched to the most critical API that depends on it |
Common mistakes
- Trusting a signature without checking whose key it is: in the Microsoft Storm-0558 key breach, tokens forged with a stolen consumer signing key were accepted for enterprise mailboxes because issuer and key scope checks were assumed rather than performed. Validate that the key belongs to the expected issuer, as RFC 8725 section 3.8 requires, and treat signing keys as the highest-value secret in a JWT design.
- Long-lived JWTs with no revocation path: a 24-hour JWT for an account that is disabled at 9am keeps working until the next day.
- Putting personal data or internal roles in JWTs sent to clients: anyone can decode them. Use opaque tokens or minimise claims.
- Clients parsing access tokens: logic that reads
expor roles from the access token breaks when the format changes. Use theexpires_inresponse value and the ID token or userinfo for user data. - An open introspection endpoint: RFC 7662 requires authorisation to call it; without that, it becomes an oracle for stolen tokens.
- Accepting any JWT from the issuer: an ID token or a token for another API accepted as an access token. Check
typandaud, as RFC 8725 section 3.12 and OWASP ASVS 5.0 V9.2.2 and V9.2.3 require.
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| OWASP ASVS 5.0 | V9.1.1, V9.1.2, V9.1.3; V9.2.1 to V9.2.4 | Signature validation, algorithm allowlist, trusted key sources; validity time, type, audience checks for self-contained tokens |
| OWASP ASVS 5.0 | V7.2.2; V8.3.2; V10.3.1 | Self-contained or reference tokens; immediate effect of authorisation changes; audience checks at resource servers |
| NIST SP 800-53 Rev. 5 | IA-5 Authenticator Management; SC-23 Session Authenticity; AC-3 Access Enforcement | Signing key and token protection; token integrity; enforcing scope and audience |
| NIST CSF 2.0 | PR.AA-03; PR.AA-05 | Authentication of users and services; enforced, least-privilege permissions |
| MITRE ATT&CK | T1528 Steal Application Access Token; T1550.001 Application Access Token; T1606 Forge Web Credentials | Theft and replay of either format; forging tokens with stolen signing keys |
Practitioner checklist
- Choose the format per audience using the decision table, and record why.
- For JWTs:
typat+jwt, algorithm allowlist, exactiss,audper API, shortexp, keys from a trusted JWKS only. - For opaque tokens: authenticated, rate-limited introspection with a cache lifetime set per API class.
- Keep personal and sensitive attributes out of client-visible JWTs, or encrypt them.
- Give JWT estates a revocation path: short lifetimes plus revocable refresh tokens, and deny lists or introspection for sensitive actions.
- Centralise external token handling at the gateway and forward internal tokens only.
- Protect and rotate signing keys, and alert on signed tokens with no issuance record.
- Add negative validation tests to CI for every API.
Standards and references
- IETF: RFC 9068 JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens
- IETF: RFC 7662 OAuth 2.0 Token Introspection
- IETF: RFC 8725 JSON Web Token Best Current Practices
- IETF: RFC 9701 JWT Response for OAuth Token Introspection
- IETF: RFC 7009 OAuth 2.0 Token Revocation
- IETF: RFC 9700 Best Current Practice for OAuth 2.0 Security
- OWASP: Application Security Verification Standard 5.0
Related NHI Mgmt Group resources: Token and Session Security Guide · JWT Validation Errors Troubleshooting Guide · Token Lifetime Policy Design Guide · API Security Guide · Signing keys and certificates in breaches