Skip to content

Bearer tokens

HTTPBearerAuth reads Authorization: Bearer <token>. It is the source for API clients, scripts and service-to-service calls. For JWTs from an identity provider, see JWT and OAuth 2.0.

Issuing and storing tokens

import os

from fastapi import FastAPI, Security

from fastapi_multiauth import (
    APIKeyCookieAuth,
    HTTPBearerAuth,
    UnauthorizedError,
    hash_token,
)

SECRET_KEY = os.environ["SECRET_KEY"]
TOKEN_HASHES: dict[str, str] = {}


async def validate_session(username: str) -> str:
    return username


async def validate_token(token: str) -> str:
    owner = TOKEN_HASHES.get(hash_token(token))
    if owner is None:
        raise UnauthorizedError()
    return owner


session = APIKeyCookieAuth(
    name="__Host-session", validator=validate_session, secret_key=SECRET_KEY
)
user_tokens = HTTPBearerAuth(validator=validate_token, prefix="user_")

app = FastAPI()


@app.post("/tokens")
async def create_token(owner: str = Security(session)):
    token = user_tokens.generate_token()  # "user_Xk3..."
    TOKEN_HASHES[hash_token(token)] = owner  # store the hash only
    return {"token": token}  # the only time anyone sees it


@app.get("/me")
async def me(owner: str = Security(user_tokens)):
    return {"owner": owner}
  • generate_token() returns 256 random bits with the source's prefix in front.
  • Store hash_token(token), never the token. SHA-256 is right for random tokens; it is not right for passwords.
  • Revoking a token is deleting its row, and it takes effect immediately.
  • Tokens are created from a browser session, never from another token, so a leaked token cannot mint more. The personal access tokens recipe also asks for a recent login.

Prefixes

prefix="user_" makes the source only accept tokens starting with user_. The check is a string comparison made before your validator runs, so a token with the wrong prefix is refused without hashing it or querying the database. Under MultiAuth, it counts as no credential for that source and the next source is tried.

Give each kind of token its own prefix and source, and combine them with MultiAuth: each validator only ever sees its own tokens. Prefixes also help secret scanners spot leaked tokens.

Token generators

generate_token() builds the token body with token_generator, then puts the prefix in front. The default is secrets.token_urlsafe; pass your own callable to change the format. It receives the number of random bytes and returns the body:

HTTPBearerAuth(validator=validate_token, prefix="user_", token_generator=secrets.token_hex)

Build it on secrets, never random. The body must be non-empty and have no leading or trailing whitespace, or generate_token() raises ValueError.

Checksummed tokens

checksum_token_generator is a ready-made generator that adds a short checksum to each token, GitHub-style. The validator can then refuse a typo without a database lookup:

from fastapi import FastAPI, Security

from fastapi_multiauth import (
    HTTPBearerAuth,
    UnauthorizedError,
    checksum_token_generator,
    hash_token,
    verify_token_checksum,
)

TOKEN_HASHES: dict[str, str] = {}


async def validate_token(token: str) -> str:
    if not verify_token_checksum(token, prefix="user_"):
        raise UnauthorizedError()  # a typo: no lookup needed
    owner = TOKEN_HASHES.get(hash_token(token))
    if owner is None:
        raise UnauthorizedError()
    return owner


user_tokens = HTTPBearerAuth(
    validator=validate_token,
    prefix="user_",
    token_generator=checksum_token_generator,
)

app = FastAPI()


@app.get("/me")
async def me(owner: str = Security(user_tokens)):
    return {"owner": owner}

Warning

Anyone can compute the checksum. It filters typos; the hash lookup is still what authenticates.