KAHIBARO
Discord Login Register

13.7. JSON Web Tokens

Why JSON Web Tokens Matter

JSON Web Tokens, usually written as JWTs and pronounced "jots", are a very popular way to implement token based authentication in backend systems.

They are used in many modern web and mobile applications, especially when you have:

In this chapter you will learn what JWTs are, what problem they solve, and how to use them correctly without going deep into topics that belong in separate chapters such as Access Tokens, Refresh Tokens, or OAuth 2.0.


What Is a JWT?

A JSON Web Token is a compact string that encodes some JSON data and is cryptographically signed.

A JWT is made of three parts, separated by dots:

text
header.payload.signature

For example:

text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTYiLCJ1c2VybmFtZSI6ImFsaWNlIiwiZXhwIjoxNzAwMDAwMDAwfQ
.
X4rnRBqcYqtwbTgBjHqQ5V9RzK0PwtH0RmL4i3Pzga0

You usually see JWTs in HTTP headers:

http
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

The Three Parts of a JWT

1. Header

The header tells:

Example header JSON:

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

This JSON is then Base64URL encoded to a string like:

text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

2. Payload

The payload contains the claims. A claim is a piece of information, such as:

Example payload JSON:

json
{
  "sub": "123456",       // subject, usually user id
  "username": "alice",
  "role": "admin",
  "exp": 1700000000
}

This JSON is also Base64URL encoded to form the second part of the token.

3. Signature

The signature makes the token tamper evident. The server signs the header and payload with a secret key.

For an HMAC algorithm like HS256, the signature is:

$$
\text{signature} = \text{HMAC\_SHA256}(\text{base64url(header)} + "." + \text{base64url(payload)}, \text{secret})
$$

The result is Base64URL encoded to become the third part of the token.

A JWT is not encrypted by default. Anyone who has the token can read its header and payload.
The signature does not hide information, it only protects integrity.
Never put passwords or other highly sensitive secrets into a JWT payload.


How JWT Authentication Works

JWTs are often used for stateless authentication. That means the server does not keep login state in memory or in a session store for each user. Instead, the client sends a JWT with every request and the server verifies it.

Typical Login Flow with JWT

  1. User logs in
    • Client sends username and password to the backend (for example with a POST /login request).
    • Backend verifies the password (using secure password hashing from other chapters, not plain text).
  2. Server creates a token
    • Backend creates a JWT that includes the user's id and maybe role or other claims.
    • Backend signs the JWT with a secret or private key.
    • Backend returns the JWT to the client.

Example JSON response:

json
   {
     "access_token": "<jwt-string-here>",
     "token_type": "bearer"
   }
  1. Client stores the token

The client can store it:

  1. Client uses the token

For every request to a protected endpoint, the client sends:

http
   GET /api/me HTTP/1.1
   Host: api.example.com
   Authorization: Bearer <jwt-string-here>
  1. Server verifies the token
    • Backend reads the Authorization header.
    • Backend verifies the signature using its secret or public key.
    • Backend checks if the token is expired.
    • If valid, backend trusts the claims inside the token (such as sub, role).
  2. Server executes the request
    • Backend executes business logic using the user identity from the token.
    • Backend returns a response.

This flow allows the backend to not store session state for each user, which helps scalability and works well with multiple servers behind a load balancer.


Standard JWT Claims

JWT defines some common claim names. You can use custom names too, but these are typical:

ClaimMeaningExample value
issIssuer, who created the token"https://api.example.com"
subSubject, who the token refers to"user-123"
audAudience, who the token is for"my-frontend-app"
expExpiration time, in seconds (UTC)1700000000
nbfNot before, token valid from this time1699990000
iatIssued at, creation time1699999000
jtiJWT ID, unique identifier"a9f9cba6-..."

In many simple systems you mainly use:

Example payload with typical claims:

json
{
  "sub": "123",
  "username": "alice",
  "role": "admin",
  "exp": 1700000000,
  "iat": 1699999000,
  "iss": "https://api.example.com"
}

Always include an expiration (exp) in your JWTs.
A token that never expires is very dangerous. If it is stolen, an attacker can use it forever.


Symmetric vs Asymmetric Signing

How you sign a JWT affects how your system is designed.

Symmetric Signing (HS256, HS384, HS512)

With symmetric signing, one shared secret key is used to:

Example algorithms:

This is simple and common in single service backends.

Example pseudocode:

python
jwt.encode(payload, secret_key, algorithm="HS256")
jwt.decode(token, secret_key, algorithms=["HS256"])

Pros

Cons

Asymmetric Signing (RS256, ES256)

With asymmetric signing, there are two keys:

Example algorithms:

Example pseudocode:

python
jwt.encode(payload, private_key, algorithm="RS256")
jwt.decode(token, public_key, algorithms=["RS256"])

Pros

Cons

For learning and small projects, HS256 is often enough. For larger systems or multiple services, RS256 or another asymmetric algorithm is often preferred.


Example: Creating and Verifying JWTs

In practice you use a library rather than manually building the Base64 strings.

Below is an example using Python with the popular PyJWT library. It is not a complete app, only a clear demonstration.

Creating a JWT

python
import jwt
from datetime import datetime, timedelta, timezone
SECRET_KEY = "super-secret-key-change-me"
def create_access_token(user_id: str, username: str) -> str:
    now = datetime.now(timezone.utc)
    payload = {
        "sub": user_id,
        "username": username,
        "exp": now + timedelta(minutes=15),
        "iat": now,
        "iss": "https://api.example.com"
    }
    token = jwt.encode(
        payload,
        SECRET_KEY,
        algorithm="HS256"
    )
    return token

Here:

Verifying a JWT

python
import jwt
from jwt import InvalidTokenError, ExpiredSignatureError
def verify_access_token(token: str) -> dict:
    try:
        payload = jwt.decode(
            token,
            SECRET_KEY,
            algorithms=["HS256"],
            issuer="https://api.example.com"
        )
        return payload
    except ExpiredSignatureError:
        raise ValueError("Token has expired")
    except InvalidTokenError:
        raise ValueError("Token is invalid")

In a real backend endpoint, you would:

  1. Read the Authorization header.
  2. Extract the token after Bearer.
  3. Call verify_access_token.
  4. Attach the user information from the payload to the request context.

Where JWTs Are Stored and Sent

JWTs are only useful if the client sends them correctly.

Common Ways to Send a JWT

LocationExampleNotes
HTTP headerAuthorization: Bearer <token>Most common for APIs
CookieSet-Cookie: access_token=<token>; HttpOnlyUseful for browsers
Query string/some-route?token=<token>Not recommended

Using the Authorization header is usually the easiest for API clients and mobile apps.

For browser based apps, using HTTP-only cookies can help protect against some attacks, but has its own details which are covered in other chapters.

Do not put tokens in URLs (query parameters) if you can avoid it.
Tokens in URLs can end up:

  • In server logs
  • In browser history
  • In referrer headers to other sites

JWT Best Practices

Using JWTs correctly is important. Many security problems come from incorrect usage, not from the JWT standard itself.

1. Use Short Expiration Times

Use relatively short lifetimes for access tokens, for example:

If a token is stolen, a short lifetime reduces the damage.

You will later combine this with refresh tokens in the dedicated chapter.

2. Validate Algorithm and Claims

When you decode a JWT:

Pseudocode:

python
payload = jwt.decode(
    token,
    key,
    algorithms=["HS256"],
    issuer="https://api.example.com",
    audience="my-frontend-app"
)

Never allow the token to decide the algorithm by itself.
Always pass the accepted algorithms explicitly to the library. This protects against some known attacks.

3. Do Not Store Sensitive Data in the Payload

JWT payloads are just Base64URL encoded. Any attacker who gets the token can read it.

Avoid storing:

Use JWTs to store identifiers, roles, and flags, not secrets.

4. Rotate Secrets and Keys

For HS256:

For RS256:

Key and secret management is covered in depth in the security and secrets management chapters, but it is important to remember that JWT security depends directly on key security.

5. Consider Token Revocation

JWTs are stateless by default. Once a token is issued, the server cannot "take it back" without extra logic.

You might need revocation when:

Common approaches:

Token revocation in detail is usually handled together with access and refresh tokens, which are covered in other chapters.


Comparing JWTs with Server Sessions

JWTs and server side sessions are both ways to keep users authenticated.

FeatureJWTServer session
Storage locationClient stores tokenServer stores session data
Server memory useNone for tokensGrows with number of sessions
StatelessYes (usually)No
RevocationHarder, needs extra logicEasier, delete session
Scaling across serversEasy, any server can verifyNeed shared session store
Token sizeUsually largerSession id is small

JWTs are very useful when:

Session based authentication can still be a good choice, especially for simple server rendered web apps.


Simple End-to-End Example

To see everything together, imagine this simplified API.

Login Endpoint

http
POST /login HTTP/1.1
Content-Type: application/json
{
  "username": "alice",
  "password": "secret-password"
}

Simplified backend logic:

python
def login(username: str, password: str):
    user = find_user_in_db(username)
    if not user or not verify_password(password, user.password_hash):
        raise HTTPException(status_code=401, detail="Invalid credentials")
    token = create_access_token(
        user_id=str(user.id),
        username=user.username
    )
    return {"access_token": token, "token_type": "bearer"}

The client receives:

json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer"
}

Protected Endpoint

Client request:

http
GET /me HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Backend logic to protect the endpoint:

python
def get_current_user(authorization_header: str):
    if not authorization_header.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="Missing token")
    token = authorization_header.split(" ", 1)[1]
    try:
        payload = verify_access_token(token)
    except ValueError:
        raise HTTPException(status_code=401, detail="Invalid or expired token")
    user_id = payload["sub"]
    user = find_user_in_db_by_id(user_id)
    if not user:
        raise HTTPException(status_code=401, detail="User not found")
    return user

The /me endpoint can then use get_current_user:

python
def me_endpoint(request):
    user = get_current_user(request.headers.get("Authorization", ""))
    return {
        "id": user.id,
        "username": user.username
    }

This shows the complete minimal flow:

  1. User logs in.
  2. Server issues JWT.
  3. Client sends JWT on each request.
  4. Server verifies token and returns data.

Summary

In the next related chapters, you will see how JWTs are used as access tokens, how refresh tokens work together with JWTs, and how these tokens integrate into complete authentication flows.

Views: 4

Comments

Please login to add a comment.

Don't have an account? Register now!