JWT Authentication — Complete Implementation Guide
In this tutorial, you will learn about JWT Authentication. We cover key concepts, practical examples, and best practices to help you master this topic.
JWT (JSON Web Token) authentication uses self-contained tokens with a signature to verify the token's integrity, allowing stateless authentication where the server does not need to store session data for every authenticated user.
What You'll Learn
By the end of this lesson, you will implement JWT access and refresh tokens, choose the right signing algorithm, verify tokens securely, handle token expiration, and avoid common JWT vulnerabilities.
Why It Matters
JWT is the dominant authentication pattern for REST APIs, mobile apps, and single-page applications. Its stateless nature scales horizontally without a session store. Doda Browser uses JWT for API authentication between its Microservices and for mobile app sessions.
Real-World Use
A mobile banking app authenticates the user with a login request. The server returns a short-lived access token (15 minutes) and a longer-lived refresh token (7 days). The app stores the access token in memory and the refresh token in secure storage. Every API request includes the access token in the Authorization header.
JWT Structure
flowchart LR
subgraph "JWT"
H[Header: alg, typ]
P[Payload: sub, iat, exp, role]
S[Signature: verify integrity]
end
H --> S
P --> S
S --> T[eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.signature]
JWT Implementation with Access and Refresh Tokens
const express = require("express");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const app = express();
app.use(express.json());
const ACCESS_SECRET = crypto.randomBytes(64).toString("hex");
const REFRESH_SECRET = crypto.randomBytes(64).toString("hex");
const refreshTokens = new Set();
app.post("/api/auth/login", (req, res) => {
const { email, password } = req.body;
if (email !== "alice@example.com" || password !== "correct-password") {
return res.status(401).json({ error: "Invalid credentials" });
}
const user = { id: 1, email, role: "user" };
const accessToken = jwt.sign(
{ sub: user.id, email: user.email, role: user.role, type: "access" },
ACCESS_SECRET,
{ expiresIn: "15m" }
);
const refreshToken = jwt.sign(
{ sub: user.id, type: "refresh" },
REFRESH_SECRET,
{ expiresIn: "7d" }
);
refreshTokens.add(refreshToken);
res.json({ accessToken, refreshToken, expiresIn: 900 });
});
app.post("/api/auth/refresh", (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken || !refreshTokens.has(refreshToken)) {
return res.status(401).json({ error: "Invalid refresh token" });
}
try {
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
const newAccessToken = jwt.sign(
{ sub: decoded.sub, role: "user", type: "access" },
ACCESS_SECRET,
{ expiresIn: "15m" }
);
res.json({ accessToken: newAccessToken, expiresIn: 900 });
} catch (err) {
refreshTokens.delete(refreshToken);
res.status(401).json({ error: "Refresh token expired" });
}
});
function authenticateJWT(req, res, next) {
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(" ")[1];
if (!token) return res.status(401).json({ error: "Token required" });
try {
const decoded = jwt.verify(token, ACCESS_SECRET);
req.user = decoded;
next();
} catch (err) {
res.status(403).json({ error: "Invalid or expired token" });
}
}
app.get("/api/profile", authenticateJWT, (req, res) => {
res.json({ userId: req.user.sub, role: req.user.role });
});
app.post("/api/auth/logout", (req, res) => {
const { refreshToken } = req.body;
if (refreshToken) refreshTokens.delete(refreshToken);
res.json({ message: "Logged out" });
});
app.listen(3000);
Expected output: Login returns two tokens. Access token authenticates API calls for 15 minutes. Refresh token issues new access tokens without re-login. Logout invalidates the refresh token.
JWT Verification in Python
import jwt
import time
class JWTAuth:
def __init__(self, secret_key):
self.secret_key = secret_key
def create_access_token(self, user_id, role="user"):
payload = {
"sub": user_id,
"role": role,
"type": "access",
"iat": int(time.time()),
"exp": int(time.time()) + 900,
}
return jwt.encode(payload, self.secret_key, algorithm="HS256")
def verify_token(self, token):
try:
payload = jwt.decode(token, self.secret_key, algorithms=["HS256"])
return payload
except jwt.ExpiredSignatureError:
raise Exception("Token expired")
except jwt.InvalidTokenError:
raise Exception("Invalid token")
def authenticate_request(self, request):
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
raise Exception("No token provided")
token = auth_header[7:]
return self.verify_token(token)
auth = JWTAuth("my-secret-key-change-in-production")
token = auth.create_access_token(1, "admin")
print(f"Token: {token[:50]}...")
payload = auth.verify_token(token)
print(f"User: {payload['sub']}, Role: {payload['role']}")
Expected output:
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
User: 1, Role: admin
Common Mistakes
- Storing JWTs in localStorage makes them accessible to XSS Attacks. Use HTTP-only cookies or in-memory storage.
- Not validating the
algheader allows attackers to change algorithm to "none" or from RS256 to HS256 using the public key. - Using overly long token lifetimes (days or weeks) without refresh token rotation.
- Not including token type (access vs refresh) in payload allows refresh tokens to be used as access tokens.
- Failing to validate the
expclaim properly or using algorithms that don't support expiration. - Including sensitive data like passwords or credit card numbers in the payload (base64 encoded, not encrypted).
Practice Questions
- What is the difference between JWT and session-based authentication?
JWT is stateless — the token contains all user information and is verified by signature. Session auth stores session data server-side. JWT scales better but cannot be revoked server-side without a blocklist.
- Why are access tokens short-lived and refresh tokens long-lived?
Short-lived access tokens limit the damage if a token is stolen. The attacker can only use it for 15 minutes. The refresh token, stored more securely, can obtain new access tokens and can be revoked.
- What is the "alg=none" attack and how do you prevent it?
An attacker modifies the JWT header to set "alg":"none", removing signature verification. Prevent by validating the algorithm against an allowlist and rejecting "none".
- Challenge: Implement JWT with refresh token rotation (new refresh token issued each refresh, old one invalidated), token family tracking for theft detection, and automatic cleanup of expired tokens.
FAQ
Mini Project: JWT Token Inspector
Build a CLI tool that decodes JWTs without verification, shows the payload, checks expiration, and validates the signature against a provided secret.
import jwt
import sys
def inspect_token(token, secret=None):
parts = token.split(".")
print("=== JWT Inspector ===")
print(f"Parts: {len(parts)} (header.payload.signature)\n")
try:
header = jwt.get_unverified_header(token)
print(f"Header: {header}")
except Exception as e:
print(f"Header decode failed: {e}")
try:
payload = jwt.decode(token, options={"verify_signature": False})
print(f"Payload: {payload}")
exp = payload.get("exp", 0)
now = int(__import__("time").time())
if exp:
remaining = exp - now
print(f"Expires: {'expired' if remaining < 0 else f'{remaining}s remaining'}")
except Exception as e:
print(f"Payload decode failed: {e}")
if secret:
try:
decoded = jwt.decode(token, secret, algorithms=["HS256"])
print(f"Signature: VALID (verified with provided secret)")
except jwt.ExpiredSignatureError:
print(f"Signature: VALID but expired")
except jwt.InvalidSignatureError:
print(f"Signature: INVALID")
if __name__ == "__main__":
token = sys.argv[1] if len(sys.argv) > 1 else "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.signature"
inspect_token(token)
What's Next
Learn about OAuth 2.0 for delegated authorization, then explore token refresh patterns for production JWT systems.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro