Skip to main content

JWT vs token introspection for MCP servers

Your MCP server gets a bearer token. Before letting the request through, it needs to know: is this token still valid, and what claims does it carry? Two paths give you those answers, and they're not the same.

What each one does

Local JWT validation — decode the token, verify its signature against the issuer's JWKS, check exp, aud, iss. No network call. The claims you trust are the ones inside the token at issuance time.

Token introspection — call POST /oauth/introspect on every request, get back a JSON document with the current state of the token (active, scope, sub, etc.). Network call per request. The claims are fresh as of the introspection moment.

Both are RFC-defined (JWT: RFC 7519; introspection: RFC 7662). Both work. They have different tradeoffs.

The tradeoff in one sentence

JWT is faster; introspection knows about revocation.

When JWT-only is right

  • Latency matters. ~50µs to verify a JWT locally vs ~5–50ms to call introspection (network + DB lookup). On a hot path, this matters.
  • Revocation is fine on a short window. If your tokens are 10 minutes long and you can accept "a stolen token works until it expires," JWT-only is fine. Most MCP deployments fall here.
  • The authorization server is occasionally unreliable. Local JWT validation works even when the auth server is down.

Use ValidationModeJWTOnly in Config.

When introspection is right

  • Revocation matters now. A user's account got compromised; you need active tokens to fail immediately, not after they expire. Only introspection sees the revocation.
  • Tokens carry sparse claims. Some OAuth servers issue opaque tokens (not JWTs at all). Introspection is the only way to learn anything about them.
  • You're enforcing per-request RBAC. A user's role changed five minutes ago; the token is stale, the introspection answer is fresh.
  • Latency budget is generous. A few extra milliseconds per call is fine.

Use ValidationModeIntrospectionOnly.

When both is right

The default in AuthSec, and usually the correct answer for production:

  • ValidationModeJWTAndIntrospect — verify the JWT locally first; if it passes, also call introspection. Either failure denies. Strictest mode; catches both forged signatures (local) and revoked-but-not-expired tokens (introspection).
  • ValidationModeJWTOrIntrospect — either path can succeed independently. Looser, useful during a gradual migration from one model to the other.

For most MCP servers, JWTAndIntrospect is the right default. The introspection call is real but it's overlapped with handler work, so the user-visible latency is dominated by the slowest path, not the sum.

The four AuthSec ValidationModes

ModeSig checkRevocation checkLatencyUse when
JWTOnlyLocal JWKSNone~50µsLatency-sensitive; revocation fine on expiry window
IntrospectionOnlyServer-sideLive~5–50msOpaque tokens; instant revocation needed
JWTAndIntrospectBothLive~5–50msDefault for production
JWTOrIntrospectEitherEitherVariableGradual migration; mixed token types

You set this via Config.ValidationMode. Defaults are inferred from which URLs you provide:

  • Both JWKSURL and IntrospectionURL set → JWTAndIntrospect
  • Only JWKSURLJWTOnly
  • Only IntrospectionURLIntrospectionOnly

See Go SDK reference.

What introspection actually returns

POST /oauth/introspect
Authorization: Basic <base64(rs_id:secret)>
Content-Type: application/x-www-form-urlencoded

token=eyJhbGc...

Response:

{
"active": true,
"sub": "alice@example.com",
"aud": "https://your-server.example.com/mcp",
"scope": "rs-xyz:tools:read rs-xyz:tools:write",
"exp": 1730000000,
"client_id": "claude-desktop"
}

The active field is the headline answer: if it's false, the token is revoked or never existed. If true, the other claims tell you who it represents.

The introspection endpoint in AuthSec also runs RBAC resolution server-side — the scope it returns is filtered to what the user currently has, not what was in the original token. This is one of the reasons production deployments lean on JWTAndIntrospect: the JWT tells you who the user said they were; introspection tells you what they can actually do right now.

Caching introspection responses

Tempting, but be careful. The whole reason to use introspection is freshness; caching defeats the point.

If latency is the issue, switch to JWTOnly instead of caching introspection. It's a cleaner tradeoff — you're explicitly accepting "revocation visible after expiry only" rather than "revocation visible after cache TTL only."

AuthSec's SDK does not cache introspection responses by default.

Revocation alternatives

If you want middle-ground revocation without per-request introspection:

  • Short token lifetimes. 5-minute tokens with refresh tokens. Revocation window is ~5 minutes. Cheap and standard.
  • Polling revocation lists. SDK fetches a "revoked token IDs" list every minute. Adds revocation visibility without per-request cost. AuthSec doesn't ship this today.
  • Push-based revocation (SETs). Authorization server pushes Security Event Tokens to subscribed resource servers when revocation happens. RFC 8417. Complex; rare in MCP deployments.

Most MCP servers pick one of (short lifetimes) or (full introspection on every request). Both are valid; the middle paths add complexity for marginal gain.

Common misconceptions

"JWTs can't be revoked at all." True for an individual JWT, but you can revoke the user (which then propagates to introspection responses, just not to local verification). Combine JWTAndIntrospect and you get instant revocation again.

"Introspection is more secure." Not really. Both verify the same authority. Introspection catches time-sensitive changes (revocation, role updates). For everything else, they're equivalent.

"Use whichever the auth server defaults to." Hydra and Keycloak both support both. The default doesn't dictate your choice; your latency budget and revocation tolerance do.