Browse Source
- Addresses issue #142 (32 👍 from community) - Add comprehensive API Key authentication tutorial - Include both simple and complete examples - Add full test coverage (7 test cases) - Integrate into existing security documentation Files added: - docs/en/docs/tutorial/security/api-key.md: Complete tutorial - docs_src/security/tutorial_api_key_header.py: Full example - docs_src/security/tutorial_api_key_simple.py: Simple example - tests/test_tutorial_api_key_header.py: Test suite Files modified: - docs/en/docs/tutorial/security/index.md: Updated navigationpull/13988/head
5 changed files with 304 additions and 0 deletions
@ -0,0 +1,108 @@ |
|||
# API Key Authentication |
|||
|
|||
There are many ways to handle security, authentication and authorization. |
|||
|
|||
But let's imagine that you have your **backend** API and you want to have a simple way to authenticate requests using an **API key** in an HTTP header. |
|||
|
|||
This is very common for APIs that provide services to other applications or microservices. |
|||
|
|||
## API Key in Header |
|||
|
|||
FastAPI provides `APIKeyHeader` to handle API key authentication using HTTP headers. |
|||
|
|||
Let's look at how to implement this: |
|||
|
|||
{* ../../docs_src/security/tutorial_api_key_header.py *} |
|||
|
|||
## What it does |
|||
|
|||
When you create an instance of `APIKeyHeader`: |
|||
|
|||
```Python |
|||
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) |
|||
``` |
|||
|
|||
* `name`: The name of the HTTP header that will contain the API key |
|||
* `auto_error`: If `True` (default), FastAPI will automatically return an error if the header is missing. If `False`, the dependency will return `None` when the header is missing. |
|||
|
|||
## The dependency |
|||
|
|||
When you use `api_key_header` as a dependency: |
|||
|
|||
```Python |
|||
def verify_api_key(api_key: str = Security(api_key_header)): |
|||
``` |
|||
|
|||
FastAPI will: |
|||
|
|||
1. Look for a header with the name you specified (in this case `X-API-Key`) |
|||
2. Extract the value from that header |
|||
3. Pass it as the `api_key` parameter to your function |
|||
|
|||
## Verification logic |
|||
|
|||
In the `verify_api_key` function: |
|||
|
|||
* We check if the API key is present |
|||
* We verify that it matches our expected value |
|||
* If invalid, we raise an `HTTPException` with status code 401 |
|||
* If valid, we return the API key (or could return user information) |
|||
|
|||
## Using the protected endpoint |
|||
|
|||
To access the protected endpoint, clients need to include the API key in the request header: |
|||
|
|||
```bash |
|||
curl -H "X-API-Key: your-secret-api-key" http://localhost:8000/protected |
|||
``` |
|||
|
|||
Without the header: |
|||
```bash |
|||
curl http://localhost:8000/protected |
|||
# Returns: 401 Unauthorized |
|||
``` |
|||
|
|||
With an invalid API key: |
|||
```bash |
|||
curl -H "X-API-Key: wrong-key" http://localhost:8000/protected |
|||
# Returns: 401 Unauthorized |
|||
``` |
|||
|
|||
## Interactive documentation |
|||
|
|||
When you go to `/docs`, you will see that your endpoints are marked as requiring authentication, and there's a way to set the API key for testing: |
|||
|
|||
1. Click the "Authorize" button |
|||
2. Enter your API key in the `APIKeyHeader` field |
|||
3. Click "Authorize" |
|||
4. Now you can test the protected endpoints directly from the docs |
|||
|
|||
## Multiple API Key methods |
|||
|
|||
You can also combine different authentication methods. FastAPI provides: |
|||
|
|||
* `APIKeyQuery`: API key in a query parameter |
|||
* `APIKeyHeader`: API key in an HTTP header (shown above) |
|||
* `APIKeyCookie`: API key in a cookie |
|||
|
|||
You can use them individually or combine them for more flexible authentication. |
|||
|
|||
## Real-world considerations |
|||
|
|||
In a production environment, you should: |
|||
|
|||
1. **Store API keys securely**: Use environment variables or a secure key management system |
|||
2. **Use strong API keys**: Generate long, random strings |
|||
3. **Implement key rotation**: Allow keys to be updated periodically |
|||
4. **Add rate limiting**: Prevent abuse of your API |
|||
5. **Log access**: Monitor who is using your API and how |
|||
6. **Use HTTPS**: Always encrypt traffic containing API keys |
|||
|
|||
## Next steps |
|||
|
|||
This is a basic example of API key authentication. For more complex scenarios, you might want to: |
|||
|
|||
* Store API keys in a database with associated user information |
|||
* Implement different permission levels for different keys |
|||
* Add expiration dates to API keys |
|||
* Combine API key authentication with other methods like OAuth2 |
|||
@ -0,0 +1,82 @@ |
|||
from fastapi import Depends, FastAPI, HTTPException, Security, status |
|||
from fastapi.security import APIKeyHeader |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
# Configuration de l'API Key - normalement vous stockeriez cela de manière sécurisée |
|||
API_KEY = "your-secret-api-key" |
|||
API_KEY_NAME = "X-API-Key" |
|||
|
|||
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) |
|||
|
|||
|
|||
class User(BaseModel): |
|||
username: str |
|||
role: str |
|||
|
|||
|
|||
def verify_api_key(api_key: str = Security(api_key_header)): |
|||
""" |
|||
Vérifie si l'API key fournie est valide. |
|||
|
|||
Args: |
|||
api_key: L'API key extraite de l'en-tête HTTP |
|||
|
|||
Returns: |
|||
L'API key si elle est valide |
|||
|
|||
Raises: |
|||
HTTPException: Si l'API key est manquante ou invalide |
|||
""" |
|||
if api_key is None: |
|||
raise HTTPException( |
|||
status_code=status.HTTP_401_UNAUTHORIZED, |
|||
detail="API Key manquante", |
|||
headers={"WWW-Authenticate": "API-Key"}, |
|||
) |
|||
if api_key != API_KEY: |
|||
raise HTTPException( |
|||
status_code=status.HTTP_401_UNAUTHORIZED, |
|||
detail="API Key invalide", |
|||
headers={"WWW-Authenticate": "API-Key"}, |
|||
) |
|||
return api_key |
|||
|
|||
|
|||
def get_current_user(api_key: str = Depends(verify_api_key)): |
|||
""" |
|||
Retourne l'utilisateur actuel basé sur l'API key valide. |
|||
|
|||
Dans un vrai système, vous feriez une requête à votre base de données |
|||
pour récupérer l'utilisateur associé à l'API key. |
|||
""" |
|||
# Simulation d'une recherche d'utilisateur en base |
|||
return User(username="john_doe", role="admin") |
|||
|
|||
|
|||
@app.get("/") |
|||
async def public_endpoint(): |
|||
"""Point d'accès public - aucune authentification requise.""" |
|||
return {"message": "Ceci est un endpoint public"} |
|||
|
|||
|
|||
@app.get("/protected") |
|||
async def protected_endpoint(current_user: User = Depends(get_current_user)): |
|||
""" |
|||
Point d'accès protégé - nécessite une API key valide. |
|||
|
|||
Pour tester cet endpoint: |
|||
curl -H "X-API-Key: your-secret-api-key" http://localhost:8000/protected |
|||
""" |
|||
return { |
|||
"message": f"Bonjour {current_user.username}!", |
|||
"user_role": current_user.role, |
|||
"protected_data": "Données sensibles accessibles uniquement avec une API key valide", |
|||
} |
|||
|
|||
|
|||
@app.get("/users/me") |
|||
async def read_current_user(current_user: User = Depends(get_current_user)): |
|||
"""Récupère les informations de l'utilisateur actuel.""" |
|||
return current_user |
|||
@ -0,0 +1,21 @@ |
|||
from fastapi import Depends, FastAPI, HTTPException, status |
|||
from fastapi.security import APIKeyHeader |
|||
|
|||
app = FastAPI() |
|||
|
|||
API_KEY = "your-secret-api-key" |
|||
|
|||
api_key_header = APIKeyHeader(name="X-API-Key") |
|||
|
|||
|
|||
def verify_api_key(api_key: str = Depends(api_key_header)): |
|||
if api_key != API_KEY: |
|||
raise HTTPException( |
|||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API Key" |
|||
) |
|||
return api_key |
|||
|
|||
|
|||
@app.get("/protected") |
|||
def read_protected_data(api_key: str = Depends(verify_api_key)): |
|||
return {"message": "This is protected data", "api_key": api_key} |
|||
@ -0,0 +1,87 @@ |
|||
from fastapi.testclient import TestClient |
|||
|
|||
from docs_src.security.tutorial_api_key_header import app |
|||
|
|||
client = TestClient(app) |
|||
|
|||
|
|||
def test_public_endpoint(): |
|||
"""Test que l'endpoint public fonctionne sans authentification.""" |
|||
response = client.get("/") |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"message": "Ceci est un endpoint public"} |
|||
|
|||
|
|||
def test_protected_endpoint_with_valid_api_key(): |
|||
"""Test de l'endpoint protégé avec une API key valide.""" |
|||
response = client.get("/protected", headers={"X-API-Key": "your-secret-api-key"}) |
|||
assert response.status_code == 200 |
|||
data = response.json() |
|||
assert "message" in data |
|||
assert "john_doe" in data["message"] |
|||
assert data["user_role"] == "admin" |
|||
assert "protected_data" in data |
|||
|
|||
|
|||
def test_protected_endpoint_without_api_key(): |
|||
"""Test de l'endpoint protégé sans API key.""" |
|||
response = client.get("/protected") |
|||
assert response.status_code == 401 |
|||
assert response.json() == {"detail": "API Key manquante"} |
|||
|
|||
|
|||
def test_protected_endpoint_with_invalid_api_key(): |
|||
"""Test de l'endpoint protégé avec une API key invalide.""" |
|||
response = client.get("/protected", headers={"X-API-Key": "wrong-key"}) |
|||
assert response.status_code == 401 |
|||
assert response.json() == {"detail": "API Key invalide"} |
|||
|
|||
|
|||
def test_users_me_endpoint_with_valid_api_key(): |
|||
"""Test de l'endpoint /users/me avec une API key valide.""" |
|||
response = client.get("/users/me", headers={"X-API-Key": "your-secret-api-key"}) |
|||
assert response.status_code == 200 |
|||
data = response.json() |
|||
assert data["username"] == "john_doe" |
|||
assert data["role"] == "admin" |
|||
|
|||
|
|||
def test_users_me_endpoint_without_api_key(): |
|||
"""Test de l'endpoint /users/me sans API key.""" |
|||
response = client.get("/users/me") |
|||
assert response.status_code == 401 |
|||
|
|||
|
|||
def test_openapi_schema(): |
|||
"""Test que le schéma OpenAPI inclut bien la sécurité API Key.""" |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200 |
|||
|
|||
openapi_schema = response.json() |
|||
|
|||
# Vérifier que le composant de sécurité API Key est présent |
|||
assert "components" in openapi_schema |
|||
assert "securitySchemes" in openapi_schema["components"] |
|||
|
|||
security_schemes = openapi_schema["components"]["securitySchemes"] |
|||
|
|||
# Rechercher le scheme APIKeyHeader |
|||
api_key_scheme = None |
|||
for _scheme_name, scheme_data in security_schemes.items(): |
|||
if scheme_data.get("type") == "apiKey" and scheme_data.get("in") == "header": |
|||
api_key_scheme = scheme_data |
|||
break |
|||
|
|||
assert api_key_scheme is not None |
|||
assert api_key_scheme["name"] == "X-API-Key" |
|||
|
|||
# Vérifier que les endpoints protégés ont bien la sécurité définie |
|||
paths = openapi_schema["paths"] |
|||
|
|||
# L'endpoint /protected devrait avoir de la sécurité |
|||
protected_endpoint = paths["/protected"]["get"] |
|||
assert "security" in protected_endpoint |
|||
|
|||
# L'endpoint public ne devrait pas avoir de sécurité |
|||
public_endpoint = paths["/"]["get"] |
|||
assert "security" not in public_endpoint or public_endpoint.get("security") == [] |
|||
Loading…
Reference in new issue