KAHIBARO
Discord Login Register

30.2. JWT Authentication

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:

JWTs give you:

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:

  1. Header
  2. Payload
  3. Signature

A JWT looks like this:

text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0IiwidXNlcm5hbWUiOiJhbm4iLCJyb2xlIjoiYWRtaW4ifQ.
VJtPRd28T0p1ZFQG4EVbP8PvHqYfOBgqv1SQfZs3pqs

It is three Base64URL-encoded strings, separated by dots.

JWT Structure

Header

The header usually has:

json
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload

The payload contains claims, which are statements about the user and the token.

Example:

json
{
  "sub": "1234",
  "username": "ann",
  "role": "admin",
  "exp": 1725026885,
  "iat": 1725026585
}

Signature

The signature ensures the token has not been tampered with.

For a token using HMAC SHA-256 (HS256), the signature is:

text
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:

ClaimMeaningExample
issIssuer, who created the token"https://api.example.com"
subSubject, usually user ID"user_123"
audAudience, who the token is for"mobile-app"
expExpiration time (Unix timestamp)1725026885
iatIssued at (Unix timestamp)1725026585
nbfNot 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:

json
{
  "sub": "user_123",
  "username": "ann",
  "role": "admin",
  "permissions": ["read:orders", "write:orders"],
  "plan": "pro"
}

Use private claims for things like:

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:

  1. Build the header.
  2. Build the payload.
  3. Encode both to Base64URL.
  4. Create the signature using your secret key and the algorithm.
  5. Concatenate the three parts with dots.

In practice you will use libraries to do this.

Algorithm Choices

Two broad types of JWT signing algorithms:

TypeExample algSecret / KeyTypical use
SymmetricHS256Single shared secret keySimple backends, same app verifies token
AsymmetricRS256Private key to sign, public key to verifyMicroservices, external verification

For most beginner APIs:

Example environment variable:

bash
JWT_SECRET="p9l3-VERY-LONG-RANDOM-SECRET-STRING-3fk29"
JWT_ALGORITHM="HS256"
JWT_EXPIRES_IN_MIN=15

Example: Creating a JWT in Python

Using PyJWT:

python
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 token

Key points:

Verifying and Decoding JWTs

When a client calls a protected API endpoint, it usually sends the token in the Authorization header:

http
GET /api/orders HTTP/1.1
Host: api.example.com
Authorization: Bearer <JWT_HERE>

Your backend must:

  1. Extract the token from the header.
  2. Verify the signature.
  3. Check expiration and any other constraints.
  4. Use the claims (for example the user ID and role) to apply authorization.

Example: Decoding and Verifying a JWT in Python

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:

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:

Why two tokens?

Typical Lifetimes

Token typeLifetime exampleWhere stored (typical)
Access token5 to 30 minutesIn memory or a secure cookie
Refresh token7 to 30 daysHttpOnly 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:

http
POST /auth/login HTTP/1.1
Content-Type: application/json
{
  "email": "ann@example.com",
  "password": "secret-password"
}

Server steps:

  1. Find user by email.
  2. Verify password (using your password hashing logic).
  3. If valid:
    • Create an access token.
    • Optionally create a refresh token.
    • Return them to the client.

Example JSON response:

json
{
  "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:

http
GET /api/me HTTP/1.1
Authorization: Bearer <ACCESS_JWT>

Server:

  1. Reads the Authorization header.
  2. Extracts the token after Bearer .
  3. Decodes and verifies it.
  4. Uses sub to get user info or load user from database.
  5. Returns data if authorized.

Step 3: Refreshing the Access Token

When the access token expires:

Example request:

http
POST /auth/refresh HTTP/1.1
Content-Type: application/json
{
  "refresh_token": "<REFRESH_JWT>"
}

Server:

  1. Verifies refresh token (signature and expiration).
  2. Optionally checks if the refresh token is still active in a database.
  3. Issues a new access token (and possibly a new refresh token).
  4. 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:

However, many real applications mix stateless JWTs with some stateful elements.

Common Options

ApproachDescriptionProsCons
Pure statelessOnly verify JWT, no token storageSimple, scalableHard to revoke tokens early
Stateful refresh tokensStore refresh tokens or their IDs in databaseCan revoke sessions, track devicesExtra database operations
Blacklist / denylistKeep a list of revoked access tokens or IDsFine-grained revocationCan grow large, maintenance needed
Versioned tokensInclude a token version in payload, check against DBSimple revocation per userStill needs DB lookup on each request

For beginners:

Authorization with Claims

JWTs are for authentication (who the user is). You can also store some authorization information in them.

Common patterns:

Example payload:

json
{
  "sub": "user_123",
  "username": "ann",
  "role": "admin",
  "permissions": ["read:orders", "write:orders"],
  "exp": 1725026885
}

On each request:

  1. Verify token.
  2. Read role or permissions.
  3. Decide if user can access the endpoint.

Example pseudo-code:

python
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:

In Python jwt.decode, always set algorithms=[ALGORITHM].

2. Weak or Hardcoded Secrets

A short or guessable secret is dangerous.

Bad example:

python
SECRET_KEY = "secret"

Better:

3. No Expiration

Tokens without exp can be valid forever if you do not enforce expiration.

Always:

4. Storing Sensitive Data in the Token

Remember, JWT payloads are only Base64URL encoded. Anyone can decode them.

Do not store:

You can store identifiers, roles, and non-sensitive flags.

5. Not Using HTTPS

If you send JWTs over plain HTTP:

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

Logging out on the client:

2. Revoking Refresh Tokens

To truly end a session:

Example refresh token table:

ColumnExample value
iduuid
user_iduser_123
token_hashHash of refresh token value
created_atDateTime
expires_atDateTime
revoked_atNullable 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:

When user logs out from all devices or password changes:

  1. Increment token_version in database.
  2. Any token with an old version becomes invalid.

Example payload:

json
{
  "sub": "user_123",
  "username": "ann",
  "token_version": 4,
  "exp": 1725026885
}

On each request:

Example Endpoints for JWT Authentication

Below is a simplified set of endpoints you might build in your authentication project.

EndpointMethodPurpose
/auth/registerPOSTCreate new user account
/auth/loginPOSTCheck credentials, return JWTs
/auth/refreshPOSTExchange refresh token for new access token
/auth/logoutPOSTRevoke refresh token / session
/auth/meGETGet current user info using access token

Example `/auth/me` Handler (Pseudo-code)

python
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 user

This pattern is similar in many frameworks:

Summary

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

Comments

Please login to add a comment.

Don't have an account? Register now!