Search Topics
Search across all FastAPI topics
API Keys
Core ConceptAPI 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.
# 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.
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.
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-1Insight
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.
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.
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