P
ProxAuth
Private beta · Join the waitlistBlog →
Security
🛡️

Implement HTTP Digest Authentication in FastAPI (RFC 7616)

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

More from the ProxAuth blog

Discover more insights about authentication and security

View all articles