Implement secure authentication and session management in backend services. Covers JWT vs. sessions, OAuth2 flows, token lifecycle, session storage, and security hardening.
Authentication and Session Management is the foundation of secure, stateful interactions between clients and backend services. It determines how users prove their identity, how systems track their activity across requests, and how access to resources is controlled and scaled. This page covers the core mechanics of authenticating users and managing their sessions: from initial login through token renewal, session invalidation, and handling failures. It assumes a distributed system with multiple services, mobile clients, and API endpoints.
A user begins authentication by submitting credentials—typically username and password—to a /login endpoint. The server validates these, creates a session, and returns a token.
POST /login HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
"username": "[email protected]",
"password": "s3cr3t!"
}
On success, the server responds with an Authorization header containing a Bearer token:
HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: session_id=abc123xyz; Path=/; HttpOnly; Secure; SameSite=Lax
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZUBleGFtcGxlLmNvbSIsImV4cCI6MTcwMDAwMDAwMH0.4N1vS9m2iX5YJ1TgkRnKZ5n1f4uXp5n3d2bKs1q5s3c
{
"user": {
"id": 42,
"name": "Alice",
"email": "[email protected]"
},
"expires_in": 3600
}
The client stores the Authorization header value and includes it in all subsequent requests:
GET /profile HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZUBleGFtcGxlLmNvbSIsImV4cCI6MTcwMDAwMDAwMH0.4N1vS9m2iX5YJ1TgkRnKZ5n1f4uXp5n3d2bKs1q5s3c
Tokens are typically JSON Web Tokens (JWT), but can be opaque or signed. A JWT consists of three parts: header, payload, and signature, separated by dots.
{
"alg": "HS256",
"typ": "JWT"
}
{
"sub": "[email protected]",
"exp": 1700000000,
"iat": 1699996400,
"scope": "read write"
}
The signature is computed over the base64-encoded header and payload, using a secret key. The client verifies the signature locally or delegates it to the server.
Tokens have a finite lifespan. When exp (expiration time) is near, clients must refresh the token. The most common method is the refresh_token flow.
The server returns a refresh_token alongside the initial access_token:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZUBleGFtcGxlLmNvbSIsImV4cCI6MTcwMDAwMDAwMH0.4N1vS9m2iX5YJ1TgkRnKZ5n1f4uXp5n3d2bKs1q5s3c",
"refresh_token": "rft_abc123xyz",
"expires_in": 3600,
"refresh_expires_in": 604800
}
To renew the token:
POST /refresh HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZUBleGFtcGxlLmNvbSIsImV4cCI6MTcwMDAwMDAwMH0.4N1vS9m2iX5YJ1TgkRnKZ5n1f4uXp5n3d2bKs1q5s3c
{
"refresh_token": "rft_abc123xyz"
}
The server validates the refresh_token, issues a new access_token, and optionally updates the refresh_token:
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZUBleGFtcGxlLmNvbSIsImV4cCI6MTcwMDA0MDAwMH0.8K3qNwDz1m8vJ7yH1lX1cDv5XpKs2h3iF1nK4c1v4p1",
"refresh_token": "rft_def456uvw",
"expires_in": 3600
}
The client uses the new access_token and stores the new refresh_token for future renewals.
For single-page applications (SPAs) and mobile apps, clients can renew tokens in the background without blocking UI. This is achieved via a refresh_token stored in memory or local storage.
On every request, the client checks the access_token's exp value. If it is within 5 minutes of expiry, the client automatically sends a refresh request before dispatching the next API call.
Failure modes:
refresh request fails due to network outage; the client uses the old token and schedules a retry.refresh request returns a 401: the refresh_token is invalid or expired. The client falls back to full login.Sessions link a user’s identity across multiple requests and services. The session state is stored either in memory, in a database, or in a distributed cache.
Sessions can be stored in:
sessions table:CREATE TABLE sessions (
id VARCHAR(255) PRIMARY KEY,
user_id INT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
data JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
Each session has a unique id, often used as a session_id cookie.
Set-Cookie: session_id=abc123xyz; Path=/; HttpOnly; Secure; SameSite=Lax
When a user logs out, changes password, or a security event occurs, sessions must be invalidated.
On logout, the server removes the session from storage and sets the Expires header on the session_id cookie:
Set-Cookie: session_id=abc123xyz; Path=/; HttpOnly; Secure; SameSite=Lax; Expires=Thu, 01 Jan 1970 00:00:00 GMT
The client sends the session_id cookie with every request. The server checks the session ID in storage. If the session is missing or expired, it returns a 401 error:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": "session_expired",
"message": "The session has expired or was invalidated."
}
The server can track session expiration via expires_at in the sessions table. On each request, it checks if now() > expires_at. If so, it sends a 410 Gone:
HTTP/1.1 410 Gone
Content-Type: application/json
{
"error": "session_expired",
"message": "The session has expired. Please log in again.",
"retry_after": 300
}
Clients can use retry_after to prompt users to log in or refresh the session.
Instead of session_id cookies, systems can use JWTs as session tokens. The JWT contains the user’s identity and session metadata.
{
"sub": "[email protected]",
"session_id": "abc123xyz",
"exp": 1700000000,
"iat": 1699996400,
"revoked": false
}
The server validates the JWT on every request. On logout or revocation, it sets revoked: true in the JWT and updates it in storage (e.g., Redis). Clients check revoked on each request and refresh the token if needed.
In a microservices architecture, a user’s session must be consistent across services. Each service validates the access_token independently.
Services can validate JWTs locally using a public key (e.g., from a jwks.json endpoint):
GET /.well-known/jwks.json HTTP/1.1
Host: auth.example.com
{
"keys": [
{
"kty": "RSA",
"kid": "abc123",
"use": "sig",
"n": "1234567890...",
"e": "AQAB"
}
]
}
Services can verify the JWT signature using the n and e values from the key.
Failure modes:
kid not in jwks.json. It returns a 401 with error: "invalid_kid".exp on the key and returns 401 if the key is expired.session_revoked flag in the database. All services check this flag on each request.A TRL entry:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZUBleGFtcGxlLmNvbSIsImV4cCI6MTcwMDAwMDAwMH0.4N1vS9m2iX5YJ1TgkRnKZ5n1f4uXp5n3d2bKs1q5s3c",
"revoked_at": "2023-11-15T10:00:00Z",
"reason": "user_logout"
}
Services can check the TRL via a GET /revoked?token={token} endpoint.
exp claim is before now(). The client returns 401 with error: "token_expired".signature does not match the header + payload. The server returns 401 with error: "invalid_signature".Authorization header: The client sends Authorization: Bearer but the token is missing. The server returns 400 Bad Request with error: "missing_token".410 Gone during the request, and the client retries the request with a fresh session.access_token is issued while an old one is still valid. Clients must handle both tokens and choose the one with the latest exp.nbf (not before) and exp claims to handle this.session_id cookies but fails to send the Authorization header. The server accepts the cookie but does not validate the token, leading to a mismatch between session state and user identity.A typical auth service configuration includes:
auth:
token:
secret: "super-secret-key-for-jwt-signing"
ttl: 3600
refresh_ttl: 604800
issuer: "api.example.com"
audience: "api.example.com"
jwks:
url: "https://auth.example.com/.well-known/jwks.json"
cache_ttl: 300
session:
storage: "redis"
redis:
host: "redis.example.com"
port: 6379
database: 0
cookie:
name: "session_id"
path: "/"
http_only: true
secure: true
same_site: "Lax"
max_age: 3600
timeout: 1800
cleanup_interval: 300
The server validates the Authorization header using the following steps:
Authorization header.Bearer token.secret or jwks.json.exp, nbf, iss, and aud.session_id.401 if any validation step fails.Clients should:
jwks.json and refresh it every cache_ttl seconds.401 and 410 responses gracefully.SameSite=Lax cookies to prevent CSRF attacks.Session management is not just authentication. It is the glue that binds user identity to service state across time and services.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy