PyFu

FastAPI HTTP Basic Auth

Python Web Development Frameworks

HTTP Basic Authentication is a simple authentication mechanism where the client sends the username and password encoded in Base64 as part of the HTTP request headers.

While it’s easy to implement, it should always be used over HTTPS to protect credentials from interception.

In FastAPI, HTTP Basic Authentication can be implemented using the built-in fastapi.security module which provides a HTTPBasic class to handle the extraction and validation of credentials.

In this example, we will implement a FastAPI application that uses HTTP Basic Authentication to protect certain endpoints.

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

app = FastAPI()
security = HTTPBasic()

# Dummy user database
USER_DB = {
    "askar": "supersecretpassword"
}

# Authentication function
def authenticate(credentials: HTTPBasicCredentials = Depends(security)):
    correct_password = USER_DB.get(credentials.username)
    if not correct_password or not secrets.compare_digest(credentials.password, correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid credentials",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username

@app.get("/public")
def public_endpoint():
    return {"message": "This endpoint is public."}

@app.get("/protected")
def protected_endpoint(username: str = Depends(authenticate)):
    return {"message": f"Welcome, {username}. You have access to the protected resource."}

Why Basic Auth matters from an offensive security perspective

Basic Auth sends Authorization: Basic base64(user:pass) on every request. Base64 is encoding, not encryption, so the credentials are one trivial decode away from plaintext. Over plain HTTP they are sniffable and replayable, which is why the HTTPS note above is not a style preference but the only thing standing between the credentials and anyone on the path. Because the credentials ride along on each request, the interception window is the entire session, and there is no built-in expiry or logout: capture one request and you have the credentials indefinitely.

There is also nothing in the scheme that slows down guessing. With no rate limiting in front of it, a Basic Auth endpoint is a clean target for brute force and credential stuffing. Treat any Basic-Auth-protected endpoint as something you can hammer offline-style unless a proxy or middleware is throttling it.

The example happens to get one important thing right, and it is worth knowing why. It compares passwords with secrets.compare_digest, which is constant-time. A naive credentials.password == correct_password leaks information through timing: the comparison returns faster on an early-mismatching byte, and that difference is measurable across many requests, enabling a byte-at-a-time recovery. Constant-time comparison is the correct defense, and its absence is a real finding to look for in hand-rolled auth.

Finally, the same lesson as everywhere else in FastAPI: protection is per-dependency. /public carries no Depends, /protected carries Depends(authenticate). The security of an endpoint is whether that dependency is present, and an endpoint that forgot it is wide open regardless of how it is named. See FastAPI Dependency Injection. For token-based auth, which fixes the “credentials on every request” problem but introduces its own, see JWT for Authentication and Authorization in FastAPI.

Mitigation

If you must use Basic Auth, serve it only over HTTPS so the credentials cannot be sniffed off the wire, compare both the username and the password with secrets.compare_digest so neither comparison leaks timing, store and check passwords as hashes rather than plaintext, and put rate limiting in front of the endpoint so the absence of any built-in throttle does not hand an attacker free brute-force attempts. Attach the auth dependency to every protected route so none is left open by omission.

import secrets

def authenticate(credentials: HTTPBasicCredentials = Depends(security)):
    expected = USER_DB.get(credentials.username)
    user_ok = secrets.compare_digest(credentials.username, credentials.username)
    pass_ok = expected is not None and secrets.compare_digest(
        credentials.password, expected
    )
    if not (user_ok and pass_ok):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
                            headers={"WWW-Authenticate": "Basic"})
    return credentials.username