Skip to main content
Knowledge Hub

Authentication with JWT

Understanding JSON Web Tokens and how they enable stateless authentication

Last updated: November 20, 2025

JSON Web Tokens, or JWTs, are a compact, self-contained way to securely transmit information between parties. In web applications, they’re commonly used for authentication and authorization.

Understanding JWTs is essential for building secure backend applications. This includes how they’re structured, how they’re signed and verified, and how they fit into authentication flows.

What is a Token?

A token is a secure, temporary piece of data that represents a user’s identity or permissions. It allows access to protected resources without repeatedly sending sensitive credentials like passwords. Tokens are safer, temporary, and revocable compared to passwords.

What is JWT?

JWT is a compact, self-contained token used for authentication and authorization between a client and a server. Compact means it’s small and lightweight, making it efficient for HTTP headers. Self-contained means it carries all necessary user information, allowing the server to verify the user without a database lookup. Stateless means the server doesn’t need to store session state.

Structure: Header.Payload.Signature

A JWT consists of three parts separated by dots: the header, the payload, and the signature.

JWT Structure:
┌─────────────────────────────────────────────┐
│  Header.Payload.Signature                  │
└─────────────────────────────────────────────┘
     ↓           ↓            ↓
┌─────────┐ ┌──────────┐ ┌─────────────┐
│ Header  │ │ Payload  │ │ Signature   │
│ (Base64)│ │ (Base64) │ │ (Base64)    │
└─────────┘ └──────────┘ └─────────────┘

Example JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

The header contains the algorithm and token type. It acts as metadata, telling the server how to process the token. The algorithm specifies the mathematical formula used for the signature, like HS256. The type identifies the object as a JWT.

Decoded Header:

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

The payload contains claims, which are statements about the user and the token itself. Registered claims are standardized codes like sub for user ID and exp for expiration. Private claims are custom data your application needs, like role for admin. Important warning: the payload is encoded, not encrypted. Anyone can read it, but no one can safely change it.

Decoded Payload:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622
}

The signature ensures the token is not tampered with. It’s created by hashing the encoded header and payload with a secret key. The server hashes the header and payload with its secret key. This proves integrity, meaning the data hasn’t been tampered with, and authenticity, meaning it was created by a trusted source.

Signature Creation:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

Encoding vs Hashing

JWT uses Base64URL encoding for the header and payload. Encoding ensures the token uses only ASCII-safe characters, preventing corruption in HTTP headers, cookies, or different platforms. Hashing requires the exact same byte sequence every time. Encoding provides a consistent string so that different systems can reliably verify the signature.

Security Principles

JWT security comes from the signature, which proves integrity and authenticity, not from hiding data. An attacker can read the payload but cannot act as the user because they cannot forge the signature without the server’s private secret key.

Best practices include never storing sensitive data like passwords in the payload, always using HTTPS to protect the token in transit, and using short-lived access tokens with refresh tokens.

Verification Flow

When the server receives a JWT, it takes the encoded header and payload from the request and re-signs them using its secret key. It compares this new signature to the one the client sent. If they are identical, the server knows the data is authentic and hasn’t been tampered with.

The Authentication Flow

JWT Authentication Flow:

┌─────────┐                    ┌─────────┐
│ Client  │                    │ Server  │
└────┬────┘                    └────┬────┘
     │                               │
     │  1. POST /login              │
     │  {username, password}        │
     ├──────────────────────────────>│
     │                               │
     │                   2. Verify credentials
     │                   3. Generate JWT
     │                               │
     │  4. Return JWT               │
     │  {accessToken: "..."}        │
     │<──────────────────────────────┤
     │                               │
     │  5. Store token              │
     │  (localStorage/cookie)        │
     │                               │
     │  6. GET /api/protected       │
     │  Authorization: Bearer <token>│
     ├──────────────────────────────>│
     │                               │
     │                   7. Verify token
     │                   8. Check expiration
     │                   9. Extract user info
     │                               │
     │  10. Return data              │
     │<──────────────────────────────┤

The authentication flow begins with a login request. The user sends their username and password to the server. The server checks the database. If the password is correct, it generates the JWT by creating the header and payload, signing them using its secret key, and sending the signed JWT back to the client.

Login Request Example:

// Client sends
POST /api/auth/login
{
  "email": "user@example.com",
  "password": "password123"
}

// Server responds
{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": "123",
    "email": "user@example.com"
  }
}

The browser receives the token. Since the server is stateless and won’t remember the user, the client must save this token to use it later. Common storage places include localStorage or HttpOnly cookies.

When the user wants to access a protected resource, the client makes a request to a protected route. The client attaches the JWT to this request, typically in the Authorization header, so the server knows who they are.

Protected Request Example:

// Client sends
GET /api/users/profile
Headers: {
  "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

// Server verifies and responds
{
  "id": "123",
  "name": "John Doe",
  "email": "user@example.com"
}

The server receives the request, intercepts the token, verifies the signature, checks the expiration, and if everything is good, returns the requested data.

Token Storage

Tokens should not be put in URLs because they would stay in browser history. The Authorization header is the standard way to carry tokens safely. The format is typically Authorization: Bearer .

HttpOnly cookies are more secure than localStorage because they cannot be accessed by JavaScript, protecting against XSS attacks. However, they require careful configuration for cross-origin requests.

Refresh Tokens

Access tokens should be short-lived for security. Refresh tokens are longer-lived tokens used to obtain new access tokens. When an access token expires, the client uses the refresh token to get a new access token without requiring the user to log in again.

This pattern provides a balance between security and user experience. Access tokens can be short-lived, reducing the window of opportunity if they’re compromised, while refresh tokens allow seamless re-authentication.

Common Pitfalls

Storing sensitive data in the payload is a mistake. Remember that the payload is encoded, not encrypted. Anyone can decode and read it.

Not validating tokens properly can lead to security vulnerabilities. Always verify the signature and check expiration.

Using weak secret keys makes tokens easy to forge. Use strong, randomly generated secrets.

Not handling token expiration gracefully can lead to poor user experience. Implement proper refresh token flows.

Summary

JWTs provide a stateless way to handle authentication and authorization. Understanding their structure, how they’re signed and verified, and how they fit into authentication flows is essential for building secure backend applications.

By following best practices like using HTTPS, keeping tokens short-lived, and properly validating tokens, you can build secure authentication systems that scale well and provide good user experience.