Auth0 Token Shapes
Auth0 issues three kinds of tokens depending on how you configure your API and request. JWT access tokens (RS256) are issued when your authorize request includes audience=YOUR_API_ID. Opaque access tokens (random strings, no structure) are issued when the audience is the/userinfoendpoint — these can't be JWT-decoded, only introspected.ID tokensare always JWTs and prove user identity to the client application. If you're pasting into a JWT decoder and seeing three segments, it's a JWT.
Standard Auth0 Access Token Claims
{
"iss": "https://YOUR_DOMAIN.auth0.com/", // trailing slash matters
"sub": "auth0|507f1f77bcf86cd799439011", // Auth0 user id
"aud": [
"https://api.yourapp.com", // your API identifier
"https://YOUR_DOMAIN.auth0.com/userinfo"
],
"iat": 1735689600,
"exp": 1735776000, // typically 24h
"azp": "0mPgj9jXk...", // client that got the token
"scope": "openid profile email read:orders", // OAuth scopes
"permissions": ["read:orders", "write:orders"], // only if RBAC enabled
"https://yourapp.com/role": "admin" // custom claims use URL namespace
}Method 1: Node.js with express-oauth2-jwt-bearer
// npm install express-oauth2-jwt-bearer
import express from 'express';
import { auth, requiredScopes } from 'express-oauth2-jwt-bearer';
const app = express();
const checkJwt = auth({
audience: 'https://api.yourapp.com',
issuerBaseURL: 'https://YOUR_DOMAIN.auth0.com/',
tokenSigningAlg: 'RS256',
});
app.get('/public', (req, res) => res.send('No auth'));
// Any valid token
app.get('/profile', checkJwt, (req, res) => {
res.json({
user: req.auth.payload.sub,
scopes: req.auth.payload.scope,
permissions: req.auth.payload.permissions,
});
});
// Scope-restricted
app.get('/orders', checkJwt, requiredScopes('read:orders'), getOrders);
// Permission-restricted (Auth0 RBAC)
function requirePermission(perm) {
return (req, res, next) => {
if (!req.auth?.payload?.permissions?.includes(perm)) {
return res.status(403).json({ error: `Missing permission: ${perm}` });
}
next();
};
}
app.post('/orders', checkJwt, requirePermission('write:orders'), createOrder);Method 2: Python / FastAPI
import httpx
from functools import lru_cache
from fastapi import Depends, HTTPException, Header
from jose import jwt
from jose.exceptions import JWTError, ExpiredSignatureError
AUTH0_DOMAIN = "YOUR_DOMAIN.auth0.com"
API_AUDIENCE = "https://api.yourapp.com"
ISSUER = f"https://{AUTH0_DOMAIN}/"
@lru_cache(maxsize=1)
def _jwks():
r = httpx.get(f"https://{AUTH0_DOMAIN}/.well-known/jwks.json", timeout=5)
r.raise_for_status()
return r.json()
def _find_key(kid):
for k in _jwks()["keys"]:
if k["kid"] == kid:
return k
_jwks.cache_clear()
for k in _jwks()["keys"]:
if k["kid"] == kid:
return k
return None
def verify(token: str) -> dict:
header = jwt.get_unverified_header(token)
key = _find_key(header["kid"])
if not key:
raise JWTError("Unknown kid")
return jwt.decode(
token, key,
algorithms=["RS256"],
audience=API_AUDIENCE,
issuer=ISSUER,
)
async def current_user(authorization: str = Header(...)) -> dict:
token = authorization.replace("Bearer ", "")
try:
return verify(token)
except ExpiredSignatureError:
raise HTTPException(401, "Token expired")
except JWTError as e:
raise HTTPException(401, f"Invalid token: {e}")
def require_permission(perm: str):
async def dep(user: dict = Depends(current_user)):
if perm not in user.get("permissions", []):
raise HTTPException(403, f"Missing permission: {perm}")
return user
return dep
# Usage
@app.get("/orders")
async def list_orders(user: dict = Depends(require_permission("read:orders"))):
return {"user": user["sub"]}Method 3: curl Walkthrough
# 1. Get a token (client credentials for M2M app)
TOKEN=$(curl -s -X POST "https://$AUTH0_DOMAIN/oauth/token" \
-H "Content-Type: application/json" \
-d "{\"client_id\":\"$CLIENT_ID\",\"client_secret\":\"$CLIENT_SECRET\",\"audience\":\"$API_AUDIENCE\",\"grant_type\":\"client_credentials\"}" \
| jq -r .access_token)
# 2. Inspect payload (local, no verify)
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .
# 3. Check JWKS keys
curl -s "https://$AUTH0_DOMAIN/.well-known/jwks.json" | jq '.keys[] | {kid, kty, alg}'
# 4. Call protected API
curl "https://api.yourapp.com/orders" -H "Authorization: Bearer $TOKEN"Common Auth0 JWT Pitfalls
- Missing trailing slash in issuer — Auth0 iss is
https://YOUR_DOMAIN.auth0.com/WITH the trailing slash. Verification fails if you drop it. - Expecting opaque tokens to decode — if you didn't pass
audienceon authorize, you get an opaque token. Add the audience param. - Using id_token for API auth — ID tokens are for the client to identify the user, not for calling APIs. Use the access token.
- Not enabling RBAC —
permissionsarray only appears if you turn on "Enable RBAC" and "Add Permissions in the Access Token" in the API settings. - Custom claims not namespaced — Auth0 strips non-standard claim keys unless they use a full URL as the key. Use
https://yourapp.com/role, notrole. - Ignoring
azp— if multiple apps share your API, validateazpto ensure only approved clients get through.
Related Tools
- JWT Decoder Online — paste-and-inspect UI
- Okta JWT Decoder — Okta token guide
- JWT RS256 Decoder — RS256 deep dive
- Firebase JWT Decoder — Firebase Auth tokens
- Verify JWT Signature — JWKS & key rotation guide