From 3797c3ca6101491e63a4fb0f80ea1eb77c9b94bb Mon Sep 17 00:00:00 2001 From: Baha Rahmouni Date: Wed, 13 Aug 2025 11:58:43 +0100 Subject: [PATCH] Add API Key Header authentication documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 navigation --- docs/en/docs/tutorial/security/api-key.md | 108 +++++++++++++++++++ docs/en/docs/tutorial/security/index.md | 6 ++ docs_src/security/tutorial_api_key_header.py | 82 ++++++++++++++ docs_src/security/tutorial_api_key_simple.py | 21 ++++ tests/test_tutorial_api_key_header.py | 87 +++++++++++++++ 5 files changed, 304 insertions(+) create mode 100644 docs/en/docs/tutorial/security/api-key.md create mode 100644 docs_src/security/tutorial_api_key_header.py create mode 100644 docs_src/security/tutorial_api_key_simple.py create mode 100644 tests/test_tutorial_api_key_header.py diff --git a/docs/en/docs/tutorial/security/api-key.md b/docs/en/docs/tutorial/security/api-key.md new file mode 100644 index 000000000..54a55ffe4 --- /dev/null +++ b/docs/en/docs/tutorial/security/api-key.md @@ -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 diff --git a/docs/en/docs/tutorial/security/index.md b/docs/en/docs/tutorial/security/index.md index d33a2b14d..aeaf0e548 100644 --- a/docs/en/docs/tutorial/security/index.md +++ b/docs/en/docs/tutorial/security/index.md @@ -103,4 +103,10 @@ FastAPI provides several tools for each of these security schemes in the `fastap In the next chapters you will see how to add security to your API using those tools provided by **FastAPI**. +You will learn about: + +* **API Key Authentication**: Simple authentication using API keys in headers, query parameters, or cookies +* **OAuth2 with Password (and hashing), Bearer with JWT tokens**: Full OAuth2 implementation with JWT tokens +* **Login system**: How to create a complete login system + And you will also see how it gets automatically integrated into the interactive documentation system. diff --git a/docs_src/security/tutorial_api_key_header.py b/docs_src/security/tutorial_api_key_header.py new file mode 100644 index 000000000..4ba585737 --- /dev/null +++ b/docs_src/security/tutorial_api_key_header.py @@ -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 diff --git a/docs_src/security/tutorial_api_key_simple.py b/docs_src/security/tutorial_api_key_simple.py new file mode 100644 index 000000000..88f4c3690 --- /dev/null +++ b/docs_src/security/tutorial_api_key_simple.py @@ -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} diff --git a/tests/test_tutorial_api_key_header.py b/tests/test_tutorial_api_key_header.py new file mode 100644 index 000000000..86cc6a3f0 --- /dev/null +++ b/tests/test_tutorial_api_key_header.py @@ -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") == []