committed by
GitHub
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