Search Topics

Search across all FastAPI topics

GitHub

API Keys

Core Concept

API keys are the simplest form of authentication. FastAPI supports them through headers, query parameters, or cookies using the Security utilities.

You protect your admin endpoint with an API key check. You compare key == 'admin-secret-key'. An attacker discovers they can time how long your comparison takes — and extract the key character by character.

terminal
# Your "secure" key check:
def verify_key(api_key: str):
    return api_key == "admin-secret-key-2024"

# The attacker's timing attack:
# "a..." → rejected in 0.2ms (first char wrong)
# "admin..." → rejected in 0.8ms (fails later = more chars matched!)
# "admin-secret-..." → rejected in 1.4ms
# After ~50 requests, they've reconstructed your entire key.

Question

Wait, how can comparing two strings leak your secret key? It sounds impossible, but it's a real attack vector. The == operator in Python stops comparing at the first mismatched character. More matching characters = slightly longer response time.

Let's build API key auth the right way — starting with the basics, then hardening it against attacks like this.

How API Key Auth Works

API keys are dead simple compared to OAuth2. There's no login flow, no token exchange. The client just includes the key with every request.

Client sends request

API key in header or query param

FastAPI extracts key

APIKeyHeader / APIKeyQuery

Your code validates

Look up key in database

Return data or 403

Invalid key = Forbidden

The Basics

What you just learned

API keys are sent with every request — no login step needed

FastAPI provides APIKeyHeader and APIKeyQuery to extract keys automatically

Invalid keys should return 403 Forbidden, not 401 Unauthorized

API keys identify the application or user making the request

Header-Based API Keys

This is the most common pattern. Clients send the API key in a custom header like X-API-Key. It keeps the key out of URLs and server logs.

main.py
from fastapi import FastAPI, Security, HTTPException
from fastapi.security import APIKeyHeader
import secrets

app = FastAPI()
api_key_header = APIKeyHeader(name="X-API-Key")

API_KEYS = {"secret-key-1": "user1", "secret-key-2": "user2"}

async def verify_api_key(api_key: str = Security(api_key_header)):
    # Use secrets.compare_digest to prevent timing attacks!
    for stored_key, user in API_KEYS.items():
        if secrets.compare_digest(api_key, stored_key):
            return user
    raise HTTPException(status_code=403, detail="Invalid API key")

@app.get("/data")
async def get_data(user: str = Security(verify_api_key)):
    return {"message": f"Hello {user}", "data": [1, 2, 3]}

Watch out

Notice we're using secrets.compare_digest() instead of ==. That one change eliminates the entire timing attack from our opening scenario. It's a single-line fix that makes your key comparison constant-time.

Try API Key Auth

Watch how FastAPI extracts and validates API keys from headers or query parameters. Try a valid key, then an invalid one — see the difference in responses.

Query Parameter API Keys

Sometimes you need the key in the URL — maybe for webhook callbacks or simple integrations. It works, but there's a catch.

main.py
from fastapi.security import APIKeyQuery

api_key_query = APIKeyQuery(name="api_key")

async def verify_api_key(api_key: str = Security(api_key_query)):
    for stored_key, user in API_KEYS.items():
        if secrets.compare_digest(api_key, stored_key):
            return user
    raise HTTPException(status_code=403, detail="Invalid API key")

# Usage: GET /data?api_key=secret-key-1

Insight

Query parameter keys end up in server logs, browser history, and referrer headers. That's why headers are preferred for production APIs. Use query params only when headers aren't an option (like generating shareable links).

Key Locations

What you just learned

Header-based keys (X-API-Key) are more secure — they don't appear in URLs

Query parameter keys are convenient but leak through logs and browser history

Always use secrets.compare_digest() for key comparison, never ==

FastAPI auto-generates Swagger UI fields for both header and query keys

Supporting Multiple Auth Methods

What if some clients send the key in a header and others use a query parameter? You can support both. The trick is auto_error=False — without it, a missing header immediately returns 403 before you can check the query param.

main.py
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
api_key_query = APIKeyQuery(name="api_key", auto_error=False)

async def verify_api_key(
    header_key: str | None = Security(api_key_header),
    query_key: str | None = Security(api_key_query),
):
    # Check header first, fall back to query param
    api_key = header_key or query_key
    if not api_key:
        raise HTTPException(status_code=403, detail="API key required")
    for stored_key, user in API_KEYS.items():
        if secrets.compare_digest(api_key, stored_key):
            return user
    raise HTTPException(status_code=403, detail="Invalid API key")

Go Deeper: Why Timing Attacks Work

Let's revisit that opening scenario. Here's the vulnerable code next to the fix, so you can see exactly what changes.

Timing Attack via String Comparison

An attacker measures response times for different API key guesses. The == operator short-circuits on the first wrong character, leaking how many characters are correct.

Broken code
main.py
async def verify_api_key(api_key: str = Security(api_key_header)):
    # BUG: == stops at first mismatch
    if api_key == "admin-secret-key-2024":
        return "admin"
    raise HTTPException(status_code=403, detail="Invalid")

Think about it...

Why use secrets.compare_digest() instead of == for comparing API keys?

Hint: Think about what information an attacker can extract from response times.

Key Points

Simple Auth

API keys are the simplest auth method — no login flow needed

Header vs Query

Headers are more secure — query params appear in logs and URLs

auto_error=False

Disable auto error to support multiple auth methods

OpenAPI Docs

Security schemes appear in Swagger UI automatically