KAHIBARO
Discord Login Register

13.8. Access Tokens

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:

You will use access tokens with:

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.

TypeWhere it livesPurposeExample length / form
Username / EmailUser input + databaseIdentify useralice@example.com
PasswordUser input, stored hashed on serverProve identity (secret)P@ssw0rd!
Session IDServer & cookieLink client to server-side session statesess_9f29a38b...
Access TokenClient, header on each requestProve user is authenticated for limited timeeyJhbGciOi... (JWT) or random string
Refresh TokenClient, sometimes cookie or storageGet new access tokens without re-loginLonger random string or JWT
API KeyClient or server configIdentify calling application or serviceRandom string like sk_live_...

Key differences:

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

  1. An opaque token
    A random string where the server keeps the meaning in its database or cache.
  2. 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:

Example of what a logical access token payload might contain (independent of any specific format):

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

Server-side, it might be stored like:

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

Cons:

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...

With JWT, the server can:

Pros:

Cons:

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

  1. Client sends credentials
    • Username and password.
    • Or social login / OAuth 2.0 grant.
    • Or an API key for machine users.
  2. Server validates credentials
    • Check that user exists.
    • Verify password hash.
    • Verify that the account is active, email verified, etc.
  3. Server creates an access token
    • Generate a random ID (opaque).
    • Or create and sign a JWT with proper claims.
  4. Set an expiration time
    • For example, 15 minutes or 1 hour.
  5. Optionally, create a refresh token
    • Longer-lived, used to get new access tokens.
  6. 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:

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

When a user logs in successfully:

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

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

http
GET /api/orders HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...

The pattern is:

text
Authorization: Bearer <access_token>

Many frameworks, including FastAPI and others, have built-in support for reading this header.

Example Request Flow

  1. Client logs in:
    • POST /auth/login with JSON body.
  2. Server responds:
    • access_token + token_type: "bearer".
  3. Client stores token:
    • In memory, or secure storage (keychain, secure storage API, HTTP-only cookie).
  4. Client calls a protected endpoint:
    • Adds Authorization: Bearer <access_token> header.
  5. Server:
    • Extracts token.
    • Verifies and decodes it.
    • Authorizes the request based on the token content.

Avoiding Common Mistakes

http
  GET /api/orders?access_token=eyJhbGciOi...

Query strings can be logged by servers and proxies and can leak in URLs.

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

  1. Check presence
    • If no token is provided, return an error like 401 Unauthorized.
  2. Check format
    • Look for Authorization: Bearer <token>.
    • If the scheme is not Bearer, return 401.
  3. Verify the token
    • If opaque, look it up in the database or cache.
    • If JWT, verify signature and decode payload.
  4. Check expiration
    • Current time must be less than exp.
  5. Check audience and issuer
    • aud matches your API.
    • iss matches your auth server.
  6. Check revocation (if needed)
    • Optionally see if the token ID is in a blocklist.
  7. Attach user info to request context
    • So your route handlers can know current_user.

Example: Validating a JWT Access Token (Conceptual)

python
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, payload

Example server logic:

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

::danger
Rule: Keep access tokens short-lived. Use refresh tokens or re-authentication for longer sessions.

Why short-lived?

Practical Example

Suppose:

Flow:

  1. User logs in, gets both:
    • Access token (expires in 15 minutes).
    • Refresh token (expires in 30 days).
  2. For the first 15 minutes:
    • Client uses the access token for API calls.
  3. 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.

Access tokens can include:

Example Token Playground

Imagine an access token payload:

json
{
  "sub": "user_42",
  "role": "customer",
  "scopes": ["read:orders", "create:orders"],
  "exp": 1716903600
}

Your API might implement rules like:

On each request:

  1. Validate the token.
  2. Check the necessary scope or role.

Pseudo authorization logic:

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

This way:

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:

EnvironmentStorage methodProsCons
BrowserHTTP-only secure cookieProtected from JS, safer vs XSSVulnerable to CSRF if not protected, more setup
BrowserLocalStorage / SessionStorageEasy to implementExposed to JavaScript, higher XSS risk
Mobile / DesktopSecure storage APIs / KeychainsProtected by OSNeeds platform-specific implementation
Server to serverEnvironment variables or config filesControlled environmentMust protect file system and config

As a backend developer you should:

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

Revocation approaches differ for opaque and JWT tokens.

Opaque Tokens Revocation

With opaque tokens, revocation is simple:

Since every request must check the token against the store, a missing or revoked entry makes the token invalid.

Example token table:

token_iduser_idrevokedexpires_at
at_abc12312302024-06-28 12:00
at_def45645612024-06-28 12:00

On each request:

  1. Look up token_id.
  2. 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:

  1. Short lifetimes
    • Keep access tokens very short-lived.
    • Rely on refresh token revocation instead.
  2. Token blacklist
    • Store revoked token IDs in a fast store like Redis.
    • Tokens contain an ID (jti, JWT ID).
    • On each request, check if jti is in the blacklist.
  3. Token versioning
    • Store a token_version in 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_version with database value; mismatch means revoke.

Example token payload with version:

json
{
  "sub": "user_42",
  "token_version": 3,
  "exp": 1716903600
}

User table:

user_idtoken_version
424

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:

  1. Start simple
    • Use opaque tokens or simple JWTs.
    • Avoid overloading tokens with too many claims initially.
  2. Use standard claims where possible
    • sub for subject (user id).
    • iat for issued at.
    • exp for expiration.
    • iss for issuer.
    • aud for audience.
  3. Keep them short-lived
    • You will almost always want access tokens to expire.
  4. Be careful with sensitive data
    • Do not put personal or secret data in tokens, especially not unencrypted.
  5. 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).
  6. 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.

  1. Login request
http
   POST /auth/login HTTP/1.1
   Content-Type: application/json
   {
     "username": "alice",
     "password": "super-secret"
   }
  1. Server response
http
   HTTP/1.1 200 OK
   Content-Type: application/json
   {
     "access_token": "eyJhbGciOi...",
     "token_type": "bearer",
     "expires_in": 900
   }
  1. Client calls protected API
http
   GET /api/profile HTTP/1.1
   Host: api.example.com
   Authorization: Bearer eyJhbGciOi...
  1. Server validates and responds
    • Validates signature and expiration.
    • Finds sub = "user_123" in payload.
    • Loads user profile or uses claims directly.

Response:

http
   HTTP/1.1 200 OK
   Content-Type: application/json
   {
     "id": 123,
     "username": "alice",
     "email": "alice@example.com"
   }
  1. Access token expires
    • After 15 minutes, token is expired.
    • Server returns:
http
   HTTP/1.1 401 Unauthorized
   WWW-Authenticate: Bearer error="invalid_token", error_description="The access token expired"
  1. Client uses refresh token or re-login
    • This is where refresh tokens come into play, which you will see in the next chapter.

Summary

Views: 5

Comments

Please login to add a comment.

Don't have an account? Register now!