13.8. Access Tokens
Table of Contents
Why Access Tokens Exist
In modern backend systems, especially APIs, you rarely keep a user “logged in” by a traditional session ID stored only on the server. Instead, you often give the client a short string that proves the user is authenticated. This string is the access token.
An access token is:
- A credential the client sends with each request.
- Something the server can verify to decide:
- Who the user is.
- Whether they are allowed to access the requested resource.
- For how long this credential is valid.
You will use access tokens with:
- Single-page applications (SPA) in the browser.
- Mobile apps.
- Machine-to-machine APIs (services calling other services).
Access tokens are central in stateless, scalable backend architectures.
::danger
Definition: An access token is a time-limited credential issued after successful authentication, which a client must present with each request to access protected resources.
Access Tokens vs Other Credentials
It is important to distinguish access tokens from other pieces involved in authentication.
| Type | Where it lives | Purpose | Example length / form |
|---|---|---|---|
| Username / Email | User input + database | Identify user | alice@example.com |
| Password | User input, stored hashed on server | Prove identity (secret) | P@ssw0rd! |
| Session ID | Server & cookie | Link client to server-side session state | sess_9f29a38b... |
| Access Token | Client, header on each request | Prove user is authenticated for limited time | eyJhbGciOi... (JWT) or random string |
| Refresh Token | Client, sometimes cookie or storage | Get new access tokens without re-login | Longer random string or JWT |
| API Key | Client or server config | Identify calling application or service | Random string like sk_live_... |
Key differences:
- Access token vs password
- Password is a long-lived secret that should never travel repeatedly on each API call.
- Access token is short-lived and is meant to be sent on each request.
- Access token vs session ID
- Session ID typically requires server-side state; the server stores data in memory or a database for that session ID.
- Access token is often used in stateless APIs; all necessary info is encoded in the token or fetched by ID.
- Access token vs refresh token
- Access token is short-lived and used on every request.
- Refresh token is long-lived and used rarely, only to get new access tokens.
::danger
Access tokens must be treated as secrets. Anyone who has a valid access token can act as that user while the token is valid.
What Information Is in an Access Token?
An access token is usually either:
- An opaque token
A random string where the server keeps the meaning in its database or cache. - A self-contained token
A structured token, like a JWT, that contains data (claims) which the server can verify.
Examples of what an access token usually represents:
- User identity
- User ID, for example
user_id = 42. - Sometimes username or email.
- Token metadata
- Issuer (
iss): which server or authorization server issued it. - Audience (
aud): which API this token is for. - Expiration (
exp): timestamp when the token becomes invalid. - Issued at (
iat): when it was created. - Authorization info
- Roles, for example
["admin", "editor"]. - Permissions, for example
["read:orders", "create:orders"]. - Scopes, for example
["profile", "email"].
Example of what a logical access token payload might contain (independent of any specific format):
{
"sub": "user_123",
"role": "customer",
"scopes": ["read:orders", "create:orders"],
"iss": "https://auth.example.com",
"aud": "https://api.example.com",
"iat": 1716900000,
"exp": 1716903600
}Note that the client often cannot or should not change these values. The server signs or stores them in a secure way.
Access Token Formats: Opaque vs JWT
Opaque Tokens
An opaque token is just a random string. The client cannot read any information from it. The server stores a mapping from the token to the user and permissions.
Example opaque access token:
at_7f2f2ad0f61441d8920a8aacaf69b3c4
Server-side, it might be stored like:
| token_id | user_id | scopes | expires_at |
|---|---|---|---|
at_7f2f2ad0f61441d8... | 123 | ["read:orders","create:orders"] | 2024-06-28 12:00 |
When the API receives this token, it looks it up in the database or cache to know who the user is and if the token is valid.
Pros:
- Simple to revoke by deleting from the database.
- Token contents are not visible to the client.
Cons:
- Requires a lookup in storage (database or cache) for every request.
- Harder to scale across multiple services without shared storage.
JWT Access Tokens
JWT stands for JSON Web Token. It is a compact, URL safe string with three parts:
<header>.<payload>.<signature>
Example JWT access token (shortened):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJyb2xlIjoiY3VzdG9tZXIiLCJleHAiOjE3MTY5MDM2MDB9.VcH...
- Header: describes the algorithm and type.
- Payload: contains claims like user ID and expiration.
- Signature: proves that the token was issued by your server and has not been tampered with.
With JWT, the server can:
- Verify the token using a secret key or public key.
- Read the user and permissions directly from the token.
- Avoid a database lookup for each request.
Pros:
- Good for stateless APIs and microservices.
- No need for central session storage for validation.
Cons:
- Revocation is trickier, because tokens are self-contained.
- If misconfigured, can leak data to the client that should be private.
::danger
Regardless of format, do not store sensitive secrets like passwords in access tokens. Only store information that is safe for the client to see, or encrypt the token if needed.
How Access Tokens Are Issued
The typical sequence for issuing an access token:
- Client sends credentials
- Username and password.
- Or social login / OAuth 2.0 grant.
- Or an API key for machine users.
- Server validates credentials
- Check that user exists.
- Verify password hash.
- Verify that the account is active, email verified, etc.
- Server creates an access token
- Generate a random ID (opaque).
- Or create and sign a JWT with proper claims.
- Set an expiration time
- For example, 15 minutes or 1 hour.
- Optionally, create a refresh token
- Longer-lived, used to get new access tokens.
- Server returns tokens to client
- In a JSON response.
- Or in an HTTP-only cookie (especially in browsers).
Example: Issuing a JWT Access Token (Conceptual Code)
Python style pseudocode:
import time
import jwt # for example purposes
SECRET_KEY = "super-secret-key"
def create_access_token(user_id: str, expires_in_seconds: int = 900):
now = int(time.time())
payload = {
"sub": user_id, # subject
"iat": now, # issued at
"exp": now + expires_in_seconds, # expiration
"scopes": ["read:profile"]
}
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
return tokenWhen a user logs in successfully:
def login(username: str, password: str):
user = get_user_by_username(username)
if not user or not verify_password(password, user.hashed_password):
raise AuthenticationError()
access_token = create_access_token(user_id=str(user.id))
return {"access_token": access_token, "token_type": "bearer"}The API response might be:
{
"access_token": "eyJhbGciOi...",
"token_type": "bearer",
"expires_in": 900
}Using Access Tokens in API Requests
Once the client has the access token, it needs to send it with each request to protected endpoints. The most common way is to use the Authorization header with the Bearer scheme.
HTTP Header Format
GET /api/orders HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...The pattern is:
Authorization: Bearer <access_token>Many frameworks, including FastAPI and others, have built-in support for reading this header.
Example Request Flow
- Client logs in:
POST /auth/loginwith JSON body.- Server responds:
access_token+token_type: "bearer".- Client stores token:
- In memory, or secure storage (keychain, secure storage API, HTTP-only cookie).
- Client calls a protected endpoint:
- Adds
Authorization: Bearer <access_token>header. - Server:
- Extracts token.
- Verifies and decodes it.
- Authorizes the request based on the token content.
Avoiding Common Mistakes
- Do not send access tokens in the query string, like:
GET /api/orders?access_token=eyJhbGciOi...Query strings can be logged by servers and proxies and can leak in URLs.
- Prefer headers or secure cookies:
- Mobile or desktop apps: use the
Authorizationheader. - Web browser apps: often use HTTP-only secure cookies to limit JavaScript access.
::danger
Rule: Use Authorization: Bearer <token> for access tokens in APIs. Never include tokens in URLs or store them in places that can be easily leaked.
Validating Access Tokens
On each incoming request that needs authentication, the backend must validate the access token.
Typical validation steps:
- Check presence
- If no token is provided, return an error like 401 Unauthorized.
- Check format
- Look for
Authorization: Bearer <token>. - If the scheme is not
Bearer, return 401. - Verify the token
- If opaque, look it up in the database or cache.
- If JWT, verify signature and decode payload.
- Check expiration
- Current time must be less than
exp. - Check audience and issuer
audmatches your API.issmatches your auth server.- Check revocation (if needed)
- Optionally see if the token ID is in a blocklist.
- Attach user info to request context
- So your route handlers can know
current_user.
Example: Validating a JWT Access Token (Conceptual)
import time
import jwt
SECRET_KEY = "super-secret-key"
def validate_access_token(token: str):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
except jwt.ExpiredSignatureError:
raise AuthenticationError("Token expired")
except jwt.InvalidTokenError:
raise AuthenticationError("Invalid token")
# Optional extra checks
now = int(time.time())
if payload.get("exp") and now >= payload["exp"]:
raise AuthenticationError("Token expired")
user_id = payload.get("sub")
if not user_id:
raise AuthenticationError("Invalid token payload")
return user_id, payloadExample server logic:
def protected_endpoint(request):
authorization = request.headers.get("Authorization")
if not authorization or not authorization.startswith("Bearer "):
raise AuthenticationError("Missing bearer token")
token = authorization.split(" ", 1)[1]
user_id, payload = validate_access_token(token)
# Continue with business logic
return {"user_id": user_id, "data": "secret info"}Token Lifetime and Expiration
Access tokens should have a limited lifetime. A very long-lived token is dangerous if it is stolen.
Typical lifetimes:
- 5 to 15 minutes for high security systems.
- 15 minutes to 1 hour for normal web APIs.
- Sometimes longer for machine-to-machine tokens, depending on use.
::danger
Rule: Keep access tokens short-lived. Use refresh tokens or re-authentication for longer sessions.
Why short-lived?
- If an attacker steals an access token, they can only use it for a short time.
- When user permissions change, new tokens reflect the change, old ones expire soon.
Practical Example
Suppose:
- Access token lifetime: 15 minutes.
- Refresh token lifetime: 30 days.
Flow:
- User logs in, gets both:
- Access token (expires in 15 minutes).
- Refresh token (expires in 30 days).
- For the first 15 minutes:
- Client uses the access token for API calls.
- After 15 minutes:
- Access token is rejected as expired.
- Client uses refresh token to get a new access token.
- User does not need to re-enter password.
You will cover refresh tokens in more detail in the dedicated chapter, but it is important to understand the relationship with access tokens.
Access Tokens and Authorization
Access tokens are closely related to authorization, not just authentication.
- Authentication confirms who the user is.
- Authorization decides what the user can do.
Access tokens can include:
role: user role (admin, user, moderator).scopesorpermissions: more fine-grained capabilities.
Example Token Playground
Imagine an access token payload:
{
"sub": "user_42",
"role": "customer",
"scopes": ["read:orders", "create:orders"],
"exp": 1716903600
}Your API might implement rules like:
- Endpoint
GET /ordersrequires scoperead:orders. - Endpoint
POST /ordersrequires scopecreate:orders. - Endpoint
GET /admin/usersrequires roleadmin.
On each request:
- Validate the token.
- Check the necessary scope or role.
Pseudo authorization logic:
def require_scope(payload, required_scope: str):
scopes = payload.get("scopes", [])
if required_scope not in scopes:
raise AuthorizationError("Missing required scope")
def get_orders(request):
payload = authenticate_request(request)
require_scope(payload, "read:orders")
# fetch and return ordersThis way:
- The access token carries the authorization info.
- You can verify it quickly.
- You do not need to load full user permissions from the database every time, unless you want to.
Be careful to update token contents when user roles change, or rely on short expiry and/or token revocation.
Where to Store Access Tokens on the Client
This is mainly a frontend concern, but it affects backend design choices.
Common storage options:
| Environment | Storage method | Pros | Cons |
|---|---|---|---|
| Browser | HTTP-only secure cookie | Protected from JS, safer vs XSS | Vulnerable to CSRF if not protected, more setup |
| Browser | LocalStorage / SessionStorage | Easy to implement | Exposed to JavaScript, higher XSS risk |
| Mobile / Desktop | Secure storage APIs / Keychains | Protected by OS | Needs platform-specific implementation |
| Server to server | Environment variables or config files | Controlled environment | Must protect file system and config |
As a backend developer you should:
- Provide guidance in your API documentation on preferred usage.
- Consider issuing tokens in secure cookies for browser-based clients.
- Implement CSRF protection if using cookies.
::danger
Never log full access tokens or store them in plain text logs. Logs can be read by admins or leaked.
Revoking Access Tokens
Revocation is the act of making a previously valid token invalid before its expiration time.
This is necessary when:
- User logs out.
- Password is changed.
- You suspect a token is stolen.
- User is disabled or deleted.
Revocation approaches differ for opaque and JWT tokens.
Opaque Tokens Revocation
With opaque tokens, revocation is simple:
- Delete the token record from the database or cache.
- Or mark its status as revoked.
Since every request must check the token against the store, a missing or revoked entry makes the token invalid.
Example token table:
| token_id | user_id | revoked | expires_at |
|---|---|---|---|
at_abc123 | 123 | 0 | 2024-06-28 12:00 |
at_def456 | 456 | 1 | 2024-06-28 12:00 |
On each request:
- Look up
token_id. - If not found or
revoked = 1, reject.
JWT Tokens Revocation
Revocation of JWTs is harder, because they are self-contained and verified only by signature and expiration.
Common strategies:
- Short lifetimes
- Keep access tokens very short-lived.
- Rely on refresh token revocation instead.
- Token blacklist
- Store revoked token IDs in a fast store like Redis.
- Tokens contain an ID (
jti, JWT ID). - On each request, check if
jtiis in the blacklist. - Token versioning
- Store a
token_versionin the user database. - Include it in tokens as a claim.
- When you want to revoke all tokens for a user, increment their
token_version. - During validation, compare token
token_versionwith database value; mismatch means revoke.
Example token payload with version:
{
"sub": "user_42",
"token_version": 3,
"exp": 1716903600
}User table:
| user_id | token_version |
|---|---|
| 42 | 4 |
If the database version (4) does not equal token version (3), treat the token as revoked.
Practical Access Token Design Tips
When designing access tokens in a backend for beginners:
- Start simple
- Use opaque tokens or simple JWTs.
- Avoid overloading tokens with too many claims initially.
- Use standard claims where possible
subfor subject (user id).iatfor issued at.expfor expiration.issfor issuer.audfor audience.- Keep them short-lived
- You will almost always want access tokens to expire.
- Be careful with sensitive data
- Do not put personal or secret data in tokens, especially not unencrypted.
- Document how to use them
- Clear instructions for clients:
- How to get a token.
- How to send it.
- How long it is valid.
- How to refresh it (when you implement refresh tokens).
- Plan for rotation
- You might need to change signing keys.
- Keep track of keys using
kid(key ID) when using JWT and multiple keys.
Example End to End Flow
Here is a simple end to end login and protected endpoint interaction using access tokens.
- Login request
POST /auth/login HTTP/1.1
Content-Type: application/json
{
"username": "alice",
"password": "super-secret"
}- Server response
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "eyJhbGciOi...",
"token_type": "bearer",
"expires_in": 900
}- Client calls protected API
GET /api/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...- Server validates and responds
- Validates signature and expiration.
- Finds
sub = "user_123"in payload. - Loads user profile or uses claims directly.
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 123,
"username": "alice",
"email": "alice@example.com"
}- Access token expires
- After 15 minutes, token is expired.
- Server returns:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="The access token expired"- Client uses refresh token or re-login
- This is where refresh tokens come into play, which you will see in the next chapter.
Summary
- Access tokens are time-limited credentials that clients send with each request to prove identity and authorization.
- They can be opaque random strings or structured tokens such as JWTs.
- A proper access token includes identity, expiration, and optional authorization information like roles and scopes.
- Clients typically send access tokens using the
Authorization: Bearer <token>header. - Servers must carefully validate tokens on every request, including signatures and expiration.
- Tokens should be short-lived, stored securely on the client, and revocable when necessary.
- Access tokens turn authentication into something that can be safely reused on many API calls without exposing long-term secrets like passwords.
Views: 5
KAHIBARO