30.2. JWT Authentication
Table of Contents
Why JWTs for Authentication?
JWT authentication is a way to let clients prove who they are without keeping session state on the server. Instead of storing a “logged in” flag in a database or memory, the server issues a digitally signed token that the client sends with each request.
You will use JWTs heavily in modern backend APIs, especially for:
- Single Page Applications (React, Vue, etc.)
- Mobile apps
- Distributed systems and microservices
JWTs give you:
- Stateless authentication, no server-side session store required.
- Self-contained tokens, they carry user ID and other claims inside.
- Easy integration with API gateways and other services.
You should already know what authentication is and how tokens generally work from earlier chapters. Here we focus on what is unique to JWTs.
What Is a JWT?
A JSON Web Token (JWT) is a compact string that encodes three parts:
- Header
- Payload
- Signature
A JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0IiwidXNlcm5hbWUiOiJhbm4iLCJyb2xlIjoiYWRtaW4ifQ.
VJtPRd28T0p1ZFQG4EVbP8PvHqYfOBgqv1SQfZs3pqsIt is three Base64URL-encoded strings, separated by dots.
JWT Structure
Header
The header usually has:
{
"alg": "HS256",
"typ": "JWT"
}algis the algorithm used to sign the token, for exampleHS256.typis the type, usually"JWT".
Payload
The payload contains claims, which are statements about the user and the token.
Example:
{
"sub": "1234",
"username": "ann",
"role": "admin",
"exp": 1725026885,
"iat": 1725026585
}subis usually the subject, often your user ID.usernameandroleare custom claims.expis the expiration time, as a Unix timestamp.iatis the issued-at time.
Signature
The signature ensures the token has not been tampered with.
For a token using HMAC SHA-256 (HS256), the signature is:
HMACSHA256(
base64urlEncode(header) + "." + base64urlEncode(payload),
secret_key
)Important rule: If an attacker can guess or obtain your secret key, they can create valid tokens for any user. Keep your JWT secret key truly secret.
JWT Claims
Claims describe properties of the token or the user. There are three main types.
Registered Claims
Standardized claim names. You do not have to use all of them, but they have special meanings.
Common registered claims:
| Claim | Meaning | Example |
|---|---|---|
iss | Issuer, who created the token | "https://api.example.com" |
sub | Subject, usually user ID | "user_123" |
aud | Audience, who the token is for | "mobile-app" |
exp | Expiration time (Unix timestamp) | 1725026885 |
iat | Issued at (Unix timestamp) | 1725026585 |
nbf | Not before (token valid from this time) | 1725026600 |
You will almost always use at least sub, iat, and exp.
Always include an exp claim and reject expired tokens. Never accept tokens without expiration for authentication.
Public Claims
These are custom claims that are not standardized but are publicly defined so they do not conflict with others. In practice, most beginner projects do not use official public claims.
Private Claims
Private claims are custom fields you define for your own application.
Examples:
{
"sub": "user_123",
"username": "ann",
"role": "admin",
"permissions": ["read:orders", "write:orders"],
"plan": "pro"
}Use private claims for things like:
roleorrolespermissionstenant_idis_email_verified
Be careful not to put sensitive data in the payload. JWT payloads are encoded, not encrypted. Anyone who has the token can read the payload.
Rule: Do not store secrets such as passwords, credit card numbers, or personal identifiers inside JWT payloads. JWTs are easily decodable.
Creating and Signing JWTs
The basic steps to create a JWT:
- Build the header.
- Build the payload.
- Encode both to Base64URL.
- Create the signature using your secret key and the algorithm.
- Concatenate the three parts with dots.
In practice you will use libraries to do this.
Algorithm Choices
Two broad types of JWT signing algorithms:
| Type | Example alg | Secret / Key | Typical use |
|---|---|---|---|
| Symmetric | HS256 | Single shared secret key | Simple backends, same app verifies token |
| Asymmetric | RS256 | Private key to sign, public key to verify | Microservices, external verification |
For most beginner APIs:
- Use HS256 with a strong random secret key.
- Store the secret in an environment variable.
Example environment variable:
JWT_SECRET="p9l3-VERY-LONG-RANDOM-SECRET-STRING-3fk29"
JWT_ALGORITHM="HS256"
JWT_EXPIRES_IN_MIN=15Example: Creating a JWT in Python
Using PyJWT:
import jwt
from datetime import datetime, timedelta, timezone
SECRET_KEY = "p9l3-VERY-LONG-RANDOM-SECRET-STRING-3fk29"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 15
def create_access_token(user_id: str, username: str, role: str = "user") -> str:
now = datetime.now(timezone.utc)
expire = now + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
payload = {
"sub": user_id,
"username": username,
"role": role,
"iat": int(now.timestamp()),
"exp": int(expire.timestamp()),
}
token = jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
return tokenKey points:
- Use timezone-aware UTC times.
- Store
subas your user identifier. - Include
iatandexp.
Verifying and Decoding JWTs
When a client calls a protected API endpoint, it usually sends the token in the Authorization header:
GET /api/orders HTTP/1.1
Host: api.example.com
Authorization: Bearer <JWT_HERE>Your backend must:
- Extract the token from the header.
- Verify the signature.
- Check expiration and any other constraints.
- Use the claims (for example the user ID and role) to apply authorization.
Example: Decoding and Verifying a JWT in Python
import jwt
from jwt import InvalidTokenError, ExpiredSignatureError
SECRET_KEY = "p9l3-VERY-LONG-RANDOM-SECRET-STRING-3fk29"
ALGORITHM = "HS256"
def decode_token(token: str) -> dict:
try:
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=[ALGORITHM],
options={"require": ["exp", "sub"]},
)
return payload
except ExpiredSignatureError:
# Token is expired
raise
except InvalidTokenError:
# Signature invalid, wrong algorithm, malformed token, etc.
raise
The jwt.decode call:
- Verifies the signature using
SECRET_KEY. - Verifies the
expclaim by default, unless disabled. - Ensures
expandsubare present if required.
Never decode JWTs without verifying the signature. Methods that simply decode Base64 and do not check the signature are not safe for authentication.
Access Tokens vs Refresh Tokens
In a JWT based system you often use two tokens:
- Access token
Short-lived, used on every request to access protected resources. - Refresh token
Long-lived, used only to get a new access token when it expires.
Why two tokens?
- Short-lived access tokens reduce damage if stolen.
- Long-lived refresh tokens let users stay logged in without re-entering passwords.
Typical Lifetimes
| Token type | Lifetime example | Where stored (typical) |
|---|---|---|
| Access token | 5 to 30 minutes | In memory or a secure cookie |
| Refresh token | 7 to 30 days | HttpOnly secure cookie or server-side store |
Do not store JWTs in localStorage in browser apps if you can avoid it. HttpOnly cookies are safer against XSS.
Login Flow with JWTs
Here is a simple example of how login works with JWTs.
Step 1: User Logs In
Client sends credentials:
POST /auth/login HTTP/1.1
Content-Type: application/json
{
"email": "ann@example.com",
"password": "secret-password"
}Server steps:
- Find user by email.
- Verify password (using your password hashing logic).
- If valid:
- Create an access token.
- Optionally create a refresh token.
- Return them to the client.
Example JSON response:
{
"access_token": "<ACCESS_JWT>",
"refresh_token": "<REFRESH_JWT>",
"token_type": "bearer",
"expires_in": 900
}Or you can send the refresh token as an HttpOnly cookie instead of in the JSON body.
Step 2: Client Calls Protected APIs
For each API call:
GET /api/me HTTP/1.1
Authorization: Bearer <ACCESS_JWT>Server:
- Reads the
Authorizationheader. - Extracts the token after
Bearer. - Decodes and verifies it.
- Uses
subto get user info or load user from database. - Returns data if authorized.
Step 3: Refreshing the Access Token
When the access token expires:
- Client calls a refresh endpoint with the refresh token.
Example request:
POST /auth/refresh HTTP/1.1
Content-Type: application/json
{
"refresh_token": "<REFRESH_JWT>"
}Server:
- Verifies refresh token (signature and expiration).
- Optionally checks if the refresh token is still active in a database.
- Issues a new access token (and possibly a new refresh token).
- Returns the new tokens.
This lets users stay logged in without logging in again.
Stateless vs Stateful JWT Authentication
JWTs are often described as stateless, meaning the server does not have to store any session data.
In a fully stateless approach:
- Server only needs the secret key to verify tokens.
- No database lookup is required to know who the user is if you trust the claims.
However, many real applications mix stateless JWTs with some stateful elements.
Common Options
| Approach | Description | Pros | Cons |
|---|---|---|---|
| Pure stateless | Only verify JWT, no token storage | Simple, scalable | Hard to revoke tokens early |
| Stateful refresh tokens | Store refresh tokens or their IDs in database | Can revoke sessions, track devices | Extra database operations |
| Blacklist / denylist | Keep a list of revoked access tokens or IDs | Fine-grained revocation | Can grow large, maintenance needed |
| Versioned tokens | Include a token version in payload, check against DB | Simple revocation per user | Still needs DB lookup on each request |
For beginners:
- Use short-lived access tokens.
- Use stateful refresh tokens stored in your database.
- Optionally load user from database on each request to verify they are active.
Authorization with Claims
JWTs are for authentication (who the user is). You can also store some authorization information in them.
Common patterns:
- A
roleclaim:"user","admin","manager". - A
permissionsclaim:["read:orders", "write:orders"].
Example payload:
{
"sub": "user_123",
"username": "ann",
"role": "admin",
"permissions": ["read:orders", "write:orders"],
"exp": 1725026885
}On each request:
- Verify token.
- Read
roleorpermissions. - Decide if user can access the endpoint.
Example pseudo-code:
def require_admin(claims: dict):
if claims.get("role") != "admin":
raise PermissionError("Admin role required")Do not trust role/permission claims blindly if they can become outdated. If roles change often, consider checking the database or using short token lifetimes so changes apply quickly.
Common JWT Security Pitfalls
JWTs are powerful, but many systems become insecure because of simple mistakes.
1. Not Validating Algorithm Properly
Some libraries support "alg": "none" for unsigned tokens.
You must make sure your verification code:
- Explicitly specifies the expected algorithm.
- Does not accept tokens that change the algorithm to
"none".
In Python jwt.decode, always set algorithms=[ALGORITHM].
2. Weak or Hardcoded Secrets
A short or guessable secret is dangerous.
Bad example:
SECRET_KEY = "secret"Better:
- Use a long random string.
- Load it from an environment variable.
- Do not commit it to Git.
3. No Expiration
Tokens without exp can be valid forever if you do not enforce expiration.
Always:
- Add
expwhen generating tokens. - Ensure your library checks expiration, or you check it.
4. Storing Sensitive Data in the Token
Remember, JWT payloads are only Base64URL encoded. Anyone can decode them.
Do not store:
- Password hashes
- Credit card numbers
- Personal ID numbers
You can store identifiers, roles, and non-sensitive flags.
5. Not Using HTTPS
If you send JWTs over plain HTTP:
- Anyone on the network can sniff the token.
- They can then use it as that user.
Always require HTTPS in production.
Logout and Token Revocation
JWTs by themselves do not have a built-in way to log out. Once a token is issued, it is valid until it expires, unless you add extra logic.
Common approaches:
1. Short Expiration for Access Tokens
- Access tokens last only a few minutes.
- If stolen, they are useful only for a short time.
Logging out on the client:
- Client deletes its stored tokens.
- Server does not need to do anything.
2. Revoking Refresh Tokens
To truly end a session:
- Store refresh tokens or a unique ID for each token in your database.
- On logout, mark that refresh token as revoked.
- On
/auth/refresh, check that the token is active.
Example refresh token table:
| Column | Example value |
|---|---|
| id | uuid |
| user_id | user_123 |
| token_hash | Hash of refresh token value |
| created_at | DateTime |
| expires_at | DateTime |
| revoked_at | Nullable DateTime |
You can store a hash instead of the raw token for extra safety.
3. Token Versioning
Add token_version or session_version to both:
- User record in database.
- JWT payload.
When user logs out from all devices or password changes:
- Increment
token_versionin database. - Any token with an old version becomes invalid.
Example payload:
{
"sub": "user_123",
"username": "ann",
"token_version": 4,
"exp": 1725026885
}On each request:
- Load user and compare
token_versionfrom DB to the JWT. - If they differ, reject the token.
Example Endpoints for JWT Authentication
Below is a simplified set of endpoints you might build in your authentication project.
| Endpoint | Method | Purpose |
|---|---|---|
/auth/register | POST | Create new user account |
/auth/login | POST | Check credentials, return JWTs |
/auth/refresh | POST | Exchange refresh token for new access token |
/auth/logout | POST | Revoke refresh token / session |
/auth/me | GET | Get current user info using access token |
Example `/auth/me` Handler (Pseudo-code)
def get_current_user(auth_header: str, db):
if not auth_header or not auth_header.startswith("Bearer "):
raise UnauthorizedError("Missing or invalid Authorization header")
token = auth_header.split(" ", 1)[1]
try:
claims = decode_token(token) # verifies signature and exp
except ExpiredSignatureError:
raise UnauthorizedError("Token expired")
except InvalidTokenError:
raise UnauthorizedError("Invalid token")
user_id = claims.get("sub")
if not user_id:
raise UnauthorizedError("Invalid token, no subject")
user = db.get_user_by_id(user_id)
if not user or not user.is_active:
raise UnauthorizedError("User not found or inactive")
return userThis pattern is similar in many frameworks:
- Extract token
- Decode and verify
- Fetch user if needed
- Attach user to the request context for the rest of the handler logic
Summary
- A JWT has three parts: header, payload, and signature.
- You use claims in the payload to store user ID, expiration, roles, and more.
- Always:
- Use a strong secret key.
- Set and verify
exp. - Verify the expected algorithm.
- Use HTTPS.
- Access tokens are short-lived and used for each API call.
- Refresh tokens are long-lived and used to obtain new access tokens.
- JWTs work well for stateless authentication, but you often combine them with some state to support logout and revocation.
- Do not put secrets in tokens, they are not encrypted.
In the next parts of the project you will implement these ideas in code, integrating JWTs into your registration, login, and protected API endpoints.
Views: 4
KAHIBARO