Token Introspection & Validation
Learn how to introspect and validate access tokens in OAuth 2.0 and OpenID Connect — practical steps, troubleshooting, and next lessons.
Focus: introspect and validate access tokens
You have a shiny access token, your API is ready to serve requests, and then it hits you: how do you actually know this token is genuine, unexpired, and not revoked? Blindly trusting a token that your resource server never verified is like letting a stranger into your house because they flashed a badge that could have been printed on a home printer. This lesson shows you how to introspect and validate access tokens so your resource server rejects forged, expired, or revoked tokens before they reach your business logic. By the end, you'll be able to implement token validation with confidence and know exactly when to use introspection versus local validation.
The problem this lesson solves
OAuth 2.0 access tokens are opaque strings to the client, but to your resource server they are the keys to your data. If you don't validate them, you open the door to:
- Forged tokens — someone crafts a random string and passes it as an access token.
- Expired tokens — a user with a stale token keeps calling your API hourly, but the token died hours ago.
- Revoked tokens — a user logs out or is banned, but the token keeps working because you never checked the authorization server.
Many developers assume that because the token came from the client, it's trustworthy. That assumption is exactly what leads to security breaches. In real-world incidents, APIs that skipped token validation allowed attackers to access resources with junk tokens. The cost? Data leaks, compliance fines, and a huge loss of trust.
Here's the core fix: before you process any request, you must answer three questions:
- Is the token authentic? Did the authorization server issue it, or is it a forgery?
- Is it still valid? Has it expired? Has it been revoked?
- Is it meant for this API? Does the audience match your resource server?
Skipping any of these checks is like leaving your back door unlocked. This lesson gives you the exact steps to validate tokens in two flavors: local validation (fast, stateless) and introspection (authoritative, stateful).
Core concept / mental model
Think of an access token as a concert ticket. The ticket has a barcode (the token value) that the venue's scanner reads at the entrance. But to know if the ticket is real, the scanner must either:
- Check the barcode's embedded data (like verifying a stamped hologram) — that's local validation, where the resource server verifies the token's signature, expiry, and audience on its own.
- Call the ticket office to confirm the ticket is valid and not reported stolen — that's token introspection, where the resource server asks the authorization server about the token's current status.
Both approaches answer the same core question: "Is this access token valid?" But they differ in accuracy and cost.
Let's define the key terms:
- Access token — a credential that authorizes a specific scope and audience. In OAuth 2.0, it's an opaque string for clients; in OpenID Connect, it's often a JWT (JSON Web Token) that can carry claims.
- Introspection endpoint — an OAuth 2.0 protected endpoint, defined in RFC 7662, that returns token metadata (active, exp, scope, client_id, etc.) when you send it a token.
- JWKS (JSON Web Key Set) — a set of public keys published by the authorization server; the resource server uses these to verify JWTs.
- Claims — pieces of information in a token, like
sub(subject) andexp(expiration).
Here's the mental model in a diagram-ish flow:
Client → Resource Server (receives access token)
├─ Option A: Validate locally (verify signature, exp, aud)
│ → fast, no extra request, but can't see revocation
└─ Option B: Introspect (POST to /introspect with token)
→ authoritative, catches revocation, but slower
The choice between local validation and introspection is a trade-off between speed and freshness. Local validation is like checking the ticket's hologram — it's fast, but if the ticket was revoked after printing, you won't know. Introspection is like calling the box office — slower but always up-to-date.
How it works step by step
Let's walk through both validation methods step by step, so you can see exactly what happens at each stage.
Local validation of a JWT access token
When your access token is a JWT (common in OpenID Connect and OAuth 2.0 with the JWT profile), you can validate it locally:
- Parse the token — split the JWT into header, payload, and signature.
- Verify the signature — using the public key from the authorization server's JWKS, check that the signature matches the token's header and payload. This proves the token wasn't tampered with.
- Check the expiration — the
expclaim must be in the future. Also checkiat(issued at) andnbf(not before) if present. - Check the audience — the
audclaim must include your API's identifier. This prevents one API's token from being used on another. - Check the issuer — the
issclaim must match your authorization server's issuer URL. This is like verifying the ticket came from the official vendor.
Pro tip: Always validate the
expclaim before using the token. Many SDKs do this automatically, but if you roll your own, never skip it. An expired token is an invalid token.
Introspection of an opaque access token
When your access token is opaque (a random string), you can't validate it locally — there's no signature to check. Instead, you must call the introspection endpoint:
- Send a request — POST the token to the
introspection_endpoint(exposed by the authorization server) with your client credentials (client_id and client_secret) for authentication. Many servers require you to identify who is asking. - Get the response — the endpoint returns a JSON object with
active(boolean), and if active, details likescope,client_id,username,exp, andaud. - Make a decision — if
activeisfalse, reject the request. Iftrue, you can optionally assert that the scopes match what your endpoint requires.
The introspection endpoint is defined in RFC 7662, and most identity providers (like Auth0, Okta, and Azure AD) expose it.
Which tokens need what?
- JWT access tokens — can be validated locally, but if you need to check revocation (e.g., user logout), you still need introspection.
- Opaque access tokens — always require introspection (or a database lookup).
- Refresh tokens — never validate in your resource server; only the authorization server handles them.
Hands-on walkthrough
Let's apply these concepts with a concrete example. We'll simulate both local JWT validation and introspection using python (or you can adapt to Node/Go). We'll use the PyJWT library for JWT, and requests for introspection.
First, install dependencies:
pip install PyJWT requests cryptography
Example 1: Local JWT validation
Assume you have a JWT issued by your authorization server. Here's a function that validates it locally using a public key from a JWKS endpoint.
import jwt
import requests
from jwt import PyJWKClient
# Your API's expected audience and issuer
AUDIENCE = "https://api.example.com"
ISSUER = "https://auth.example.com/"
# Replace with your Authorization Server's JWKS URL
jwks_client = PyJWKClient("https://auth.example.com/.well-known/jwks.json")
def validate_jwt(token):
try:
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=AUDIENCE,
issuer=ISSUER,
options={"require": ["exp", "sub"]}
)
# Token is valid. Use payload claims.
print("Token is valid. Subject:", payload.get("sub"))
print("Scopes:", payload.get("scope", ["none"]))
return payload
except jwt.ExpiredSignatureError:
print("Token has expired.")
except jwt.InvalidAudienceError:
print("Token audience does not match this API.")
except jwt.InvalidIssuerError:
print("Token issuer mismatch.")
except jwt.PyJWTError as e:
print(f"Token invalid: {e}")
return None
Expected output (when token is expired):
Token has expired.
Example 2: Token introspection
Now, let's introspect an opaque access token. We'll use the requests library to call the introspection endpoint.
import requests
INTROSPECTION_URL = "https://auth.example.com/oauth2/introspect"
CLIENT_ID = "my-api-client"
CLIENT_SECRET = "your-client-secret"
def introspect_token(token):
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"token": token,
"token_type_hint": "access_token",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET
}
response = requests.post(INTROSPECTION_URL, data=data, headers=headers)
response.raise_for_status() # raise for 4xx/5xx
result = response.json()
if result.get("active"):
print("Token is active. Scopes:", result.get("scope"))
print("Expires at:", result.get("exp"))
return result
else:
print("Token is not active (expired, revoked, or invalid).")
return None
Expected output (realistic):
Token is active. Scopes: read write
Expires at: 1712345678
Putting it together in a resource server
In a real Flask/FastAPI endpoint, you'd call these functions before processing the request:
from flask import Flask, request, jsonify
app = Flask(__name__)
def authorize_request():
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return None, 401
token = auth_header.split(" ")[1]
# Try local validation first, then introspection if local fails
payload = validate_jwt(token)
if payload:
return payload, 200
# If local validation failed (maybe token is opaque), introspect
result = introspect_token(token)
if result and result.get("active"):
return result, 200
return None, 401
@app.route("/api/data")
def get_data():
payload, status = authorize_request()
if status != 200:
return jsonify({"error": "Unauthorized"}), 401
# Now serve the data
return jsonify({"data": "secret"})
Pro tip: Always prefer local validation for JWT tokens because it's stateless and fast. Use introspection as a fallback or when you need to check revocation.
Compare options / when to choose what
Let's compare local JWT validation vs. introspection in a quick-reference table:
| Aspect | Local JWT validation | Token introspection |
|---|---|---|
| Speed | Fast (no HTTP request) | Slower (extra HTTP request) |
| Freshness | Cannot see revocation | Sees revoked tokens |
| Token type | JWT only | Opaque and JWT |
| Network dependency | Requires JWKS fetch (cached) | Requires every call to AS |
| Complexity | Moderate (crypto, claim checks) | Simple (HTTP POST) |
| Best for | High-throughput APIs with JWT | APIs that need real-time revocation |
When to choose local validation: - You're using JWT access tokens. - Your API handles many requests per second and can't afford a round trip to the authorization server. - You can tolerate slight delay in revocation (e.g., tokens expire quickly, like 15 minutes).
When to choose introspection: - You're using opaque tokens. - You need to instantly revoke access when a user logs out or is banned. - Your authorization server provides an introspection endpoint and you're okay with the latency.
Variations (maybe as a bullet list): - JWT access tokens with optional revocation: Use local validation plus a short token lifetime (e.g., 5 minutes) and add a revocation list for 'logout' events. - Hybrid approach: Validate JWT locally for fast path, and call introspection only for sensitive operations (like changing passwords). - Opaque tokens in API gateways: Many gateways (e.g., Kong, Envoy) have built-in introspection plugins, saving you code.
Troubleshooting & edge cases
Let's address common pitfalls and weird edge cases you'll hit when you introspect and validate access tokens.
"I get 401 even though the token looks valid"
- Check clock skew: Your server and the authorization server may have different times. Use a reasonable clock skew (e.g., 30 seconds) when checking
exp. - Audience mismatch: Make sure your
audienceparameter matches exactly theaudclaim. A missing trailing slash can break it. - Algorithm confusion: Ensure the
algin the token header matches one of the allowed algorithms. Don't acceptnone.
"The token is expired but introspection says active"
This can happen if the authorization server uses a different time. Set nbf and exp checks based on the server's time, or rely on the active flag and exp from introspection response.
"Revoked tokens still work"
If you're using local validation only, you'll never see revocations. To fix, either switch to introspection or implement a revocation list (a denylist) checked before serving a request.
"The introspection endpoint returns 401"
You likely didn't authenticate properly. Many introspect endpoints require client credentials in the Authorization header as basic auth, not in the body. Use:
curl -u client_id:client_secret -d "token=..." https://auth.example.com/oauth2/introspect
"JWKS fetch is slow"
Cache the JWKS keys and refresh them periodically (e.g., every hour). Use the kid header to select the right key.
Edge case: Multiple audiences
Some tokens have a list of audiences. Ensure your API's identifier is in that list:
if AUDIENCE not in payload.get("aud", []):
raise jwt.InvalidAudienceError
What you learned & what's next
You now know two powerful ways to introspect and validate access tokens: local JWT validation for speed and introspection for authority. You can check a token's signature, expiration, audience, and revocation status — the four pillars of token trust. You learned when to pick one over the other and how to troubleshoot common failures like expired tokens, audience mismatches, and clock skew.
Next lesson in this track: we'll cover refresh tokens and token rotation — how to keep users logged in seamlessly while keeping your tokens short-lived and safe. You'll see how the validation you just mastered fits into a complete token lifecycle. Stay tuned!
Remember: a token you don't validate is just a random string. Make validation a non-negotiable step in every API request.
Practice recap
Hands-on exercise: Set up a simple Flask API that protects an endpoint using both local JWT validation and introspection. Issue a test JWT (you can generate one with PyJWT), call the endpoint with a valid, expired, and tampered token, and observe the different responses. Then, modify the introspection function to reject a revoked token and test it. This will cement the difference between the two methods.
Common mistakes
- Skipping expiration checks on JWT tokens — always verify the
expclaim, preferably with a small clock skew. - Trusting an opaque token without calling the introspection endpoint — opaque tokens have no signature, so local validation is impossible.
- Using the same audience for multiple APIs without checking the
audclaim — a token issued for one API should never be accepted by another. - Caching JWKS keys forever and never refreshing — keys rotate, so you must refresh the key set or you'll reject valid tokens.
- Introspecting every request even when using JWT, which adds unnecessary latency — use local validation for JWT and only introspect when needed.
Variations
- Use a Reverse Proxy / API Gateway with built-in introspection support (e.g., Kong, Envoy) to offload token validation.
- Implement a hybrid approach: validate JWT locally for the fast path, and call introspection only for high-risk operations like password change.
- For OAuth 2.0 with opaque tokens, some servers also support the
token_type_hintparameter to speed up lookup; use it when available.
Real-world use cases
- A banking API validates JWT access tokens locally to meet sub-50ms latency, but introspects the token before processing a money transfer to ensure it wasn't revoked.
- A microservices architecture where each service validates JWTs against a shared JWKS endpoint, ensuring consistent audience checks across all services.
- A mobile app backend that uses opaque tokens and introspects them on every API call to instantly revoke access when users log out or are banned.
Key takeaways
- Token validation is the gatekeeper of your API — always verify authenticity, expiry, audience, and revocation.
- Local JWT validation is fast and stateless; use it when you have JWT tokens and can tolerate slight revocation delay.
- Token introspection is authoritative and catches revocation, but costs an extra HTTP round trip — use it for opaque tokens or sensitive operations.
- Always check the
audclaim to prevent token reuse across different APIs. - Clock skew can cause false 'expired' errors; allow a small tolerance (e.g., 30 seconds).
- Cache JWKS keys but refresh them periodically to handle key rotation gracefully.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.