JWT Token Authentication — Signed Claims for Stateless API Security
In this tutorial, you will learn about JWT Token Authentication. We cover key concepts, practical examples, and best practices to help you master this topic.
JWT (JSON Web Token) authentication uses signed tokens containing user identity and claims, enabling stateless API security without server-side session storage.
What You'll Learn
How JWT tokens work for API authentication, the structure of a JWT, how signing prevents tampering, and implementation patterns for login and protected endpoints.
Why It Matters
Unlike opaque tokens that require server-side lookup, a JWT is self-contained. The server verifies the signature and reads user claims directly from the token — no database query needed. This makes JWT ideal for distributed systems and Microservices.
Real-World Use
Auth0, Firebase, and Google APIs all use JWT for authentication. Durga Antivirus Pro uses JWT for its dashboard API — each request carries user role and permissions in the token itself.
flowchart LR
A["Client"] -->|"POST /login"| B["Auth Server"]
B -->|"JWT: header.payload.signature"| A
A -->|"GET /data\nBearer JWT"| C["API Server"]
C -->|"Verify signature"| D["Valid JWT?"]
D -->|"Yes — extract claims"| E["200 OK"]
D -->|"No"| F["401 Unauthorized"]
style A fill:#dbeafe,stroke:#2563eb
style B fill:#fef3c7,stroke:#d97706
style C fill:#fef3c7,stroke:#d97706
style E fill:#dcfce7,stroke:#16a34a
style F fill:#fecaca,stroke:#dc2626
JWT Structure
A JWT has three base64url-encoded parts separated by dots:
header.payload.signature
The header specifies the signing algorithm. The payload contains claims (user ID, role, expiry). The signature verifies the token has not been tampered with.
Code Example: JWT Authentication with PyJWT
import jwt
import datetime
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRET_KEY = "your-secret-key-change-in-production"
USERS = {"admin": "password123"}
@app.route("/api/login", methods=["POST"])
def login():
data = request.get_json()
password = USERS.get(data.get("username"))
if not password or password != data.get("password"):
return jsonify({"error": "Invalid credentials"}), 401
token = jwt.encode({
"sub": data["username"],
"role": "admin",
"iat": datetime.datetime.utcnow(),
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}, SECRET_KEY, algorithm="HS256")
return jsonify({"token": token, "expires_in": 3600})
def require_auth(f):
def wrapper(*args, **kwargs):
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
return jsonify({"error": "Missing token"}), 401
try:
token = auth[7:]
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
request.user = payload
except jwt.ExpiredSignatureError:
return jsonify({"error": "Token expired"}), 401
except jwt.InvalidTokenError:
return jsonify({"error": "Invalid token"}), 401
return f(*args, **kwargs)
wrapper.__name__ = f.__name__
return wrapper
@app.route("/api/profile")
@require_auth
def profile():
return jsonify({
"user": request.user["sub"],
"role": request.user["role"]
})
if __name__ == "__main__":
app.run()
Expected output:
$ curl -X POST -H "Content-Type: application/json" \
-d '{"username":"admin","password":"password123"}' \
http://localhost:5000/api/login
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...","expires_in":3600}
$ curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
http://localhost:5000/api/profile
{"user":"admin","role":"admin"}
Common Mistakes
1. Storing Secrets in Code
Hardcoding JWT signing secrets in source code exposes them to anyone with Repository access. Use environment variables or a secrets manager.
2. Not Validating the Expiration
Without exp validation, an attacker can use an old JWT forever. PyJWT validates exp automatically when present.
3. Using Weak Secret Keys
Short or dictionary-word secrets can be brute-forced. Use at least 256 bits of entropy (32 bytes from secrets.token_bytes).
4. Including Passwords in JWT Payload
The payload is base64-encoded, not encrypted. Anyone with the token can decode the payload. Never include passwords or secrets.
5. Not Using Access + Refresh Token Pattern
Short-lived access tokens with long-lived refresh tokens balance security and usability. Without refresh tokens, users must log in every hour.
Practice Questions
- What three parts make up a JWT?
- How does the server verify a JWT without a database lookup?
- What happens when a JWT expires?
- Why should the JWT payload not contain passwords?
- What is the purpose of the
subclaim?
Answers:
- Header (algorithm, type), Payload (claims), Signature (verification hash).
- The server recomputes the signature using the shared secret or public key. If the computed signature matches the token's signature, the token is valid.
- The server returns 401. The client should use a refresh token to obtain a new access token.
- The payload is only base64-encoded — anyone can decode it. Secrets must never be in the payload.
- The
sub(subject) claim identifies the user. It is typically the user ID or username.
Challenge: Implement JWT authentication with access tokens (15 min expiry) and refresh tokens (7 day expiry). Include token refresh and logout (add refresh token to blocklist).
FAQ
Mini Project
Create a Flask application with JWT authentication: login endpoint that issues a signed JWT, a @require_auth decorator for protected routes, and a profile endpoint that reads user claims from the token.
What's Next
JWT is covered in depth in the JWT Complete Guide. Next, learn about Session Cookie Authentication for traditional web app auth.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro