Authentication and authorization in FastAPI

Build FastAPI credential checks with HTTP Basic, password hashes, validated Bearer JWTs, user lookup, and role dependencies, with clear production boundaries.

Best practices
Illustration of coordinated application tasks

Authentication establishes who made a request. Authorization decides whether that identity may perform the requested operation. FastAPI supplies dependencies for extracting credentials and describing security schemes in OpenAPI; your application still owns password verification, token validation, user lookup, and access rules.

This guide walks through HTTP Basic, password hashing, Bearer JWT validation, and a role dependency. These are separate building blocks. The examples do not constitute a complete identity provider or implement a token-login endpoint.

Understand what a security dependency provides

HTTPBasic, OAuth2PasswordBearer, and APIKeyHeader extract credentials and perform the checks associated with their scheme. They also describe that scheme in generated API documentation. Merely declaring a dependency does not validate a password, verify a JWT signature, or look up an API key.

Choose the credential type to suit the client and identity system. For third-party sign-in, use an appropriate identity provider and an authorization-code flow with PKCE. A local password example should not be mistaken for a complete OAuth authorization server.

Try HTTP Basic with demonstration credentials

In a Python environment, install FastAPI first:

pip install "fastapi[standard]"

This small application illustrates credential comparison and the authentication challenge:

import secrets
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()
security = HTTPBasic()

@app.get("/admin")
def admin(creds: HTTPBasicCredentials = Depends(security)):
    username_ok = secrets.compare_digest(
        creds.username.encode("utf-8"), b"admin"
    )
    password_ok = secrets.compare_digest(
        creds.password.encode("utf-8"), b"s3cret"
    )
    if not (username_ok and password_ok):
        raise HTTPException(
            401, "Authentication failed",
            headers={"WWW-Authenticate": "Basic"},
        )
    return {"message": "Administrator authenticated"}

The fixed credentials are for a local demonstration. Production code should verify credentials against protected user records and use HTTPS. HTTP Basic sends credentials on each request; base64 encoding does not encrypt them. Its operational fit depends on the client and threat model, rather than simply whether the tool is labeled “internal.”

Both comparisons run before their results are combined. Converting input to bytes avoids compare_digest rejecting non-ASCII strings when comparing strings directly. FastAPI's HTTP Basic parser has its own credential-decoding rules; this conversion is not a promise that every Unicode credential is accepted by that parser. See FastAPI's HTTP Basic documentation.

Store password hashes

Install the password-hashing and JWT packages:

pip install pyjwt 'pwdlib[argon2]'

Use a password-hashing function rather than storing or reversibly encrypting the password:

from pwdlib import PasswordHash

password_hash = PasswordHash.recommended()
stored_hash = password_hash.hash("example-user-password")
valid = password_hash.verify("example-user-password", stored_hash)

Persist the hash in the user record. During login, retrieve that record, verify the supplied password, check that the account is active, and only then issue credentials. Apply rate limits and a consistent failure response so the login endpoint does not become a convenient password-guessing or account-enumeration interface.

Hashing is deliberately expensive. Keep it off an asynchronous event loop's direct execution path and size the worker capacity for the expected login load. Password reset, hash upgrades, and account recovery need their own application flows.

Issue and verify short-lived Bearer tokens

A JWT carries claims protected by a signature. A signed token is not encrypted: do not put secrets or unnecessary personal data in its payload.

Add the following to the application. Before starting it, supply JWT_SECRET from protected configuration. For HS256, use a cryptographically random secret with at least 256 bits of entropy, shared only with the systems that must sign or verify these tokens.

from datetime import datetime, timedelta, timezone
import os
import jwt
from fastapi.security import OAuth2PasswordBearer

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
SECRET = os.environ["JWT_SECRET"]
ALGORITHM = "HS256"

def create_token(subject: str) -> str:
    payload = {
        "sub": subject,
        "exp": datetime.now(timezone.utc) + timedelta(minutes=30),
    }
    return jwt.encode(payload, SECRET, algorithm=ALGORITHM)

def get_current_user(token: str = Depends(oauth2_scheme)) -> str:
    try:
        payload = jwt.decode(
            token,
            SECRET,
            algorithms=[ALGORITHM],
            options={"require": ["exp", "sub"]},
        )
        subject = payload["sub"]
        if not isinstance(subject, str) or not subject:
            raise jwt.InvalidTokenError("Invalid subject")
        return subject
    except jwt.PyJWTError:
        raise HTTPException(
            401, "Invalid or expired token",
            headers={"WWW-Authenticate": "Bearer"},
        )

@app.get("/me")
def me(subject: str = Depends(get_current_user)):
    return {"subject": subject}

The decoder uses a server-selected algorithm allowlist and requires both expiration and subject claims. Choose a stable user identifier for sub. This dependency returns the validated subject; it has not yet proved that the corresponding account still exists or remains active.

tokenUrl="token" declares where API documentation should obtain a token. It does not create POST /token. Implement that endpoint separately, including password verification and its request/response contract, before expecting the documentation's login flow to work. Never expose create_token as an endpoint that accepts an arbitrary username and signs it without authentication.

In a system with a defined issuer or audience, validate those claims too, using the values configured for your application. Restrict accepted keys and plan key rotation. The FastAPI JWT tutorial and PyJWT validation reference cover the underlying APIs.

Load the user before checking roles

An authenticated subject is an input to authorization, not the authorization decision itself. A reusable role dependency can load the current account and check its permissions:

def require_role(role: str):
    def checker(subject: str = Depends(get_current_user)):
        # Application-defined lookup: return a user with is_active and roles.
        user = load_user_by_subject(subject)
        if user is None or not user.is_active:
            raise HTTPException(
                401, "Account unavailable",
                headers={"WWW-Authenticate": "Bearer"},
            )
        if role not in user.roles:
            raise HTTPException(403, "Insufficient permissions")
        return user
    return checker

@app.delete("/users/{uid}")
def delete_user(uid: int, admin=Depends(require_role("admin"))):
    # Implement deletion, tenant/object checks, and auditing in your service.
    return {"authorized_user_id": uid, "deleted": False}

load_user_by_subject is an application function to implement with your database layer, such as SQLAlchemy or SQLModel. The endpoint intentionally reports deleted: False: it demonstrates the dependency and does not delete a record.

A role check alone may be too broad. Multi-tenant applications also need to verify the caller's relationship to the target organization and object. Loading roles from current user records can make permission changes effective on the next request, while roles embedded in a token may remain stale until the token expires or is rejected by another control.

Operate the authentication path

Enforce HTTPS, rate-limit login attempts, protect signing keys, and define how to revoke access. Short-lived access tokens reduce the lifetime of a stolen token; refresh-token rotation and revocation still require explicit state and lifecycle handling. A denylist is another option when immediate token rejection is required.

Keep audit events focused: authentication outcome, authorization decision, request identifier, and the operation or object involved. Do not record passwords, complete tokens, or credential-bearing headers. In Guance, collect those records with the DataKit log collector and verify a deliberate 401 and 403 before building queries. Preserve the distinction between missing or invalid identity and an identity that lacks permission.

Test missing credentials, wrong passwords, invalid signatures, expired tokens, missing claims, inactive accounts, and insufficient roles. A valid token with the wrong audience should fail when audience validation is part of the application's contract. Tests should also confirm that a denied request cannot execute the protected operation.

Common questions

Should I use JWTs or server-side sessions?

Choose based on client integration and revocation requirements. A session identifier can refer to server-held state that is straightforward to revoke. Self-contained JWT validation can reduce per-request token-state lookups, but immediate revocation and current account checks often reintroduce state. Both approaches need secure transport and careful client storage; cookie-based credentials also require the relevant CSRF protections.

Where should signing keys live?

Use a secret-management system or appropriately protected runtime configuration, never the repository. With asymmetric signing such as RS256, verifiers use a public key while the issuer keeps the private key. That can improve separation between services, but it still needs trusted key distribution, algorithm restrictions, and rotation.

Is OAuth2PasswordBearer a complete OAuth2 implementation?

No. It declares the security scheme and extracts the Bearer token. The application or identity provider supplies login, issuance, signature and claim validation, and authorization. Use a supported provider integration for third-party login rather than extending this demonstration into a custom protocol implementation.

Related reading

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting