JWT Access Tokens — Short-Lived Tokens for Stateless API Authorization
In this tutorial, you will learn about JWT Access Tokens. We cover key concepts, practical examples, and best practices to help you master this topic.
JWT access tokens are short-lived tokens containing user identity and authorization claims, sent with every API request to prove the client is authenticated and authorized.
What You'll Learn
How access tokens work, what claims to include, appropriate expiry times, and how to validate access tokens on every request.
Why It Matters
Access tokens are the primary credential for API requests. Getting their structure, expiry, and validation right is critical for both security and user experience. Too short = poor UX. Too long = security risk.
Real-World Use
GitHub API tokens expire after configurable periods. Google API tokens last 3600 seconds. Durga Antivirus Pro uses 15-minute access tokens for its dashboard API — short enough to limit breach damage, long enough for smooth UX.
flowchart LR
A["Client"] -->|"Request + Access Token"| B["API Server"]
B -->|"Validate signature"| C["Valid?"]
C -->|"Yes"| D{"Check expiry"}
D -->|"Not expired"| E["Check scopes"]
E -->|"Sufficient"| F["200 OK"]
C -->|"No"| G["401 Unauthorized"]
D -->|"Expired"| G
E -->|"Insufficient"| H["403 Forbidden"]
style A fill:#dbeafe,stroke:#2563eb
style B fill:#fef3c7,stroke:#d97706
style F fill:#dcfce7,stroke:#16a34a
style G fill:#fecaca,stroke:#dc2626
style H fill:#fecaca,stroke:#dc2626
Access Token Claims
{
"iss": "https://auth.dodatech.com",
"sub": "user-123456",
"aud": "https://api.dodatech.com",
"exp": 1718000000,
"iat": 1717996400,
"scope": "threat:read threat:write",
"role": "analyst"
}
Code Example: Access Token Validation Middleware
from flask import Flask, request, jsonify, g
import jwt, datetime, os
app = Flask(__name__)
SECRET = os.environ.get("JWT_SECRET")
ISSUER = "https://auth.dodatech.com"
AUDIENCE = "https://api.dodatech.com"
def require_access_token(f):
def wrapper(*args, **kwargs):
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
return jsonify({"error": "Missing access token"}), 401
token = auth[7:]
try:
payload = jwt.decode(
token,
SECRET,
algorithms=["HS256"],
options={
"require": ["exp", "iss", "aud"],
"verify_exp": True
},
issuer=ISSUER,
audience=AUDIENCE
)
# Check token type
if payload.get("token_type") != "access":
return jsonify({"error": "Invalid token type"}), 401
g.user = payload
except jwt.ExpiredSignatureError:
return jsonify({"error": "Access token expired"}), 401
except jwt.InvalidAudienceError:
return jsonify({"error": "Invalid audience"}), 401
except jwt.InvalidIssuerError:
return jsonify({"error": "Invalid issuer"}), 401
except jwt.InvalidTokenError:
return jsonify({"error": "Invalid access token"}), 401
return f(*args, **kwargs)
return wrapper
@app.route("/api/threats")
@require_access_token
def get_threats():
return jsonify({
"user": g.user["sub"],
"role": g.user.get("role"),
"scopes": g.user.get("scope")
})
Access Token Lifetime Guidelines
| Context | Recommended TTL | Reasoning |
|---|---|---|
| Standard web app | 15 minutes | Good balance of security and UX |
| Mobile app | 1 hour | Less frequent refresh (battery) |
| High-security API | 5 minutes | Limit breach damage |
| Internal microservice | 1 hour | Trusted network, less churn |
| Third-party integration | 30 minutes | Standard OAuth2 practice |
Common Mistakes
1. Making Access Tokens Too Long
24-hour access tokens defeat the purpose of short-lived tokens. If stolen, the attacker has a day of access. Use 15 minutes.
2. Not Including Token Type
Without token_type: access in the payload, a refresh token could be used as an access token. Always include the token type.
3. Not Validating aud and iss
A token issued for service A should not work for service B. Validate audience and issuer on every request.
4. Sending Access Tokens in URLs
Passing ?token=xxx in the URL exposes the token in logs and browser history. Always use the Authorization header.
5. Not Handling Expired Tokens Gracefully
The client should silently refresh the token and retry. Returning a raw 401 with no instructions creates a poor UX.
Practice Questions
- How long should an access token typically live?
- What claims should every access token include?
- How does the client send the access token to the server?
- What happens when an access token expires?
- Why should access tokens include a token_type claim?
Answers:
- 15 minutes for most applications. Adjust based on security requirements and refresh frequency.
iss(issuer),sub(subject/user),aud(audience),exp(expiry),iat(issued at), andscope(permissions).- Via the
Authorization: Bearer <token>HTTP header. Never in URL query parameters. - The server returns 401. The client should use a refresh token to obtain a new access token and retry.
- To prevent token confusion — a refresh token or ID token should not be accepted as an access token.
Challenge: Build a Flask middleware that validates access tokens, checks expiry, verifies audience and issuer, and returns specific error messages for each failure case.
FAQ
Mini Project
Build a Flask API with JWT access token authentication. Include a login endpoint that issues access tokens, a middleware that validates them, and protected endpoints that use token claims for authorization.
What's Next
Now learn about JWT Refresh Tokens — the counterpart to access tokens for maintaining sessions.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro