HTTP Digest Authentication improves on Basic Auth because the password is never sent directly — instead, the client and server compute an MD5-based response using a server-provided nonce. FastAPI does not include Digest Auth natively, so we implement a minimal RFC-compliant flow.
This example is simplified for internal API usage and demonstrates the core concepts: nonce generation, challenge response, and signature verification.
Digest Auth Example (Minimal Implementation)
from fastapi import FastAPI, Request, HTTPException, status
from hashlib import md5
import os
import time
import base64
app = FastAPI()
USERNAME = "admin"
PASSWORD = "s3cr3t"
REALM = "FastAPI Secure Area"
def generate_nonce():
return base64.b64encode(os.urandom(16)).decode()
def ha1(username, realm, password):
return md5(f"{username}:{realm}:{password}".encode()).hexdigest()
def ha2(method, uri):
return md5(f"{method}:{uri}".encode()).hexdigest()
def compute_response(ha1_value, ha2_value, nonce):
return md5(f"{ha1_value}:{nonce}:{ha2_value}".encode()).hexdigest()
@app.middleware("http")
async def digest_auth(request: Request, call_next):
auth = request.headers.get("Authorization")
if not auth:
return challenge()
if not auth.startswith("Digest "):
raise HTTPException(status_code=400, detail="Invalid auth header")
# Parse key=value pairs
params = dict(
item.strip().replace('"', "").split("=")
for item in auth[len("Digest "):].split(",")
)
nonce = params.get("nonce")
username = params.get("username")
uri = params.get("uri")
client_resp = params.get("response")
if not all([nonce, username, uri, client_resp]):
return challenge()
expected = compute_response(
ha1(USERNAME, REALM, PASSWORD),
ha2(request.method, uri),
nonce,
)
if client_resp != expected:
return challenge()
return await call_next(request)
def challenge():
nonce = generate_nonce()
header = (
f'Digest realm="{REALM}", qop="auth", nonce="{nonce}", algorithm=MD5'
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Unauthorized",
headers={"WWW-Authenticate": header},
)
@app.get("/secure-digest")
def secure_digest():
return {"message": "Digest authentication successful."}Notes
Advantages
- Password is never transmitted directly
- Prevents replay attacks via nonce
- More secure than Basic Auth
Limitations
- MD5-based (legacy)
- No built-in support in browsers for advanced options (qop=auth-int, SHA-256)
- Suitable mainly for internal services