Major changes across backend, frontend, infrastructure: - Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud) - Admin panel: user management, pricing, settings - Glossary system with CSV import/export - Subscription and tier quota management - Security hardening (rate limiting, API key auth, path traversal fixes) - Docker compose for dev, prod, and IONOS deployment - Alembic migrations for new tables - Frontend: dashboard, pricing page, landing page, i18n (en/fr) - Test suite and verification scripts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
20 KiB
Story 3.4: Authentification API via X-API-Key
Status: done
Story
En tant que système, Je veux authentifier les requêtes API via le header X-API-Key, de sorte que les clients d'automatisation puissent accéder à l'API sans JWT.
Acceptance Criteria
- Header X-API-Key: Les requêtes peuvent inclure
X-API-Key: sk_live_...pour l'authentification. (FR32) - Authentification Valide: Si la clé est valide et active, la requête procède avec le contexte utilisateur.
- Clé Invalide: Si la clé n'existe pas dans la base, retourne 401 avec erreur
INVALID_API_KEY. - Clé Révoquée: Si la clé existe mais
is_active=False, retourne 401 avec erreurAPI_KEY_REVOKED. - Clé Expirée: Si la clé a une date
expires_atdans le passé, retourne 401 avec erreurAPI_KEY_EXPIRED. - Clé Manquante: Si aucune clé n'est fournie et pas de JWT, retourne 401 avec erreur
MISSING_API_KEY(selon contexte). - Mise à Jour Usage: À chaque utilisation réussie,
last_used_atetusage_countsont mis à jour. - Coexistence JWT: L'authentification par JWT (web users) et X-API-Key (automation) coexistent sur les mêmes endpoints.
Tasks / Subtasks
-
Task 1: Créer un Middleware/Dépendance Réutilisable (AC: #1, #2, #7, #8)
- 1.1 Créer
middleware/api_key_auth.pyavec dépendancerequire_api_key - 1.2 Extraire la logique de
get_user_from_api_keyde translate_routes.py vers le middleware - 1.3 Ajouter la mise à jour de
last_used_atetusage_count(déjà dans auth_service.py) - 1.4 Créer
get_authenticated_user_unifiedqui essaie API key puis JWT
- 1.1 Créer
-
Task 2: Standardiser les Réponses d'Erreur (AC: #3, #4, #5, #6)
- 2.1 Créer des exceptions structurées pour chaque cas d'erreur
- 2.2 Format uniforme:
{error: "CODE", message: "..."} - 2.3 S'assurer que tous les endpoints utilisent le même format
-
Task 3: Appliquer aux Endpoints Existants (AC: #8)
- 3.1 Remplacer
get_authenticated_userdans translate_routes.py par la nouvelle dépendance - 3.2 Vérifier que tous les endpoints /api/v1/* supportent l'auth API key
- 3.3 Documenter les endpoints qui nécessitent auth vs optionnels
- 3.1 Remplacer
-
Task 4: Ajouter les Tests (AC: Tous)
- 4.1 Test authentification réussie avec clé valide
- 4.2 Test erreur avec clé invalide (401, INVALID_API_KEY)
- 4.3 Test erreur avec clé révoquée (401, API_KEY_REVOKED)
- 4.4 Test erreur avec clé expirée (401, API_KEY_EXPIRED)
- 4.5 Test coexistence JWT et API key (priorité API key si les deux présents)
- 4.6 Test mise à jour de last_used_at et usage_count
- 4.7 Test endpoint sans auth (si applicable)
Dev Notes
Infrastructure Existante (Ne pas réimplémenter)
Fonction get_user_by_api_key (services/auth_service.py lignes 290-330):
def get_user_by_api_key(api_key: str) -> Optional[User]:
"""
Get a user by API key.
Verifies that:
- The key exists in the database
- The key is active (is_active=True)
- The key hasn't expired (expires_at is None or in the future)
Returns the user associated with the API key, or None if invalid/revoked.
Raises:
ValueError: With code "API_KEY_REVOKED" if key exists but is inactive
"""
Fonction get_user_from_api_key (routes/translate_routes.py lignes 145-175):
async def get_user_from_api_key(
x_api_key: Optional[str] = Header(None, alias="X-API-Key"),
) -> Optional[Any]:
"""
Get user from X-API-Key header if provided.
Raises:
HTTPException: 401 with API_KEY_REVOKED if key was revoked
"""
Fonction get_authenticated_user (routes/translate_routes.py lignes 178-185):
async def get_authenticated_user(
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
x_api_key: Optional[str] = Header(None, alias="X-API-Key"),
) -> Optional[Any]:
"""Get authenticated user from JWT or API key."""
user = await get_user_from_api_key(x_api_key)
if user:
return user
return await get_current_user_optional(credentials)
Modèle ApiKey (database/models.py)
class ApiKey(Base):
__tablename__ = "api_keys"
id = Column(String(36), primary_key=True)
user_id = Column(String(36), ForeignKey("users.id"))
name = Column(String(100))
key_hash = Column(String(255)) # SHA256 du clé
key_prefix = Column(String(10)) # First 8 chars for identification
is_active = Column(Boolean, default=True) # ⭐ Pour révocation
scopes = Column(JSON, default=list) # ["translate", "read", "write"]
last_used_at = Column(DateTime) # ⭐ Mis à jour à chaque utilisation
usage_count = Column(Integer, default=0) # ⭐ Compteur d'utilisation
created_at = Column(DateTime)
expires_at = Column(DateTime) # ⭐ Pour expiration
revoked_at = Column(DateTime) # Set when is_active=False
Architecture Actuelle
translate_routes.py
├── get_user_from_api_key() # Extrait X-API-Key header, appelle auth_service
├── get_authenticated_user() # Essaie API key, puis JWT
└── translate_document_v1() # Utilise get_authenticated_user comme dépendance
Architecture Proposée
middleware/
└── api_key_auth.py
├── require_api_key() # Dépendance qui REQUIERT une clé API
├── get_user_from_api_key() # Extrait et valide (optionnel)
└── get_authenticated_user() # Unifie API key + JWT (optionnel)
routes/
├── translate_routes.py # Utilise get_authenticated_user
├── api_key_routes.py # Utilise JWT (gestion des clés)
└── auth_routes.py # Utilise JWT (login/register)
Patterns à Suivre
Format de Réponse Erreur (déjà établi):
{
"error": "INVALID_API_KEY",
"message": "Clé API invalide ou non reconnue."
}
Codes Erreur API Key:
| Code | HTTP | Condition |
|---|---|---|
INVALID_API_KEY |
401 | Clé non trouvée dans DB |
API_KEY_REVOKED |
401 | is_active=False |
API_KEY_EXPIRED |
401 | expires_at < now |
MISSING_API_KEY |
401 | Aucune clé fournie (si requis) |
Ordre de Priorité Auth:
- Si
X-API-Keyheader présent → Utiliser API key auth - Sinon si
Authorization: Bearer→ Utiliser JWT auth - Sinon → Utilisateur non authentifié (None)
Structure de Fichiers
middleware/
├── __init__.py
├── api_key_auth.py # CRÉER - Dépendances auth réutilisables
├── error_handler.py # Existant
├── rate_limit.py # Existant
├── security.py # Existant
├── tier_quota.py # Existant
└── validation.py # Existant
routes/
├── translate_routes.py # MODIFIER - Utiliser nouvelle dépendance
├── api_key_routes.py # Existant (pas de changement)
└── auth_routes.py # Existant (pas de changement)
tests/
└── test_story_3_4_api_key_authentication.py # CRÉER
Project Structure Notes
- Le middleware d'authentification API key doit être réutilisable dans tous les endpoints
- Les endpoints de gestion de clés (api_key_routes.py) utilisent JWT, pas API key
- L'authentification API key est pour les endpoints "métier" (translate, etc.)
Références
- [Source: _bmad-output/planning-artifacts/epics.md#Story 3.4]
- [Source: _bmad-output/planning-artifacts/architecture.md#Authentication & Security]
- [Source: services/auth_service.py#get_user_by_api_key (lignes 290-330)]
- [Source: routes/translate_routes.py#get_user_from_api_key (lignes 145-175)]
- [Source: database/models.py#ApiKey]
Intelligence de la Story Précédente (3.3)
Ce qui a été implémenté
- Endpoint Admin DELETE pour révocation à
DELETE /api/v1/admin/api-keys/{key_id} - Soft delete avec
is_active=Falseetrevoked_attimestamp - Audit logging avec admin_id, key_id, owner_user_id, reason
- Tests complets dans
tests/test_story_3_3_admin_api_key_revocation.py
Patterns Établis à Réutiliser
from fastapi.responses import JSONResponse
# Format erreur structuré
return JSONResponse(
status_code=401,
content={
"error": "API_KEY_REVOKED",
"message": "Cette clé API a été révoquée.",
},
)
Points d'Attention Identifiés
- NE PAS utiliser
HTTPExceptionavecdetailstring - UtiliserJSONResponsestructuré - TOUJOURS snake_case dans les réponses JSON
- VÉRIFIER que
get_user_by_api_keylèveValueErroravec le bon code
Intelligence Git (Commits Récents)
Derniers commits pertinents:
- Story 3.1: Génération de clés API avec
secrets.token_urlsafe(32) - Story 3.2: Révocation utilisateur avec soft delete
- Story 3.3: Révocation admin avec audit logging
Patterns identifiés:
- Tests avec
pytestdanstests/ - Fixtures dans
tests/conftest.py - Mocking de la DB pour tests unitaires
Contexte Métier
Epic 3: API & Automation (Pro)
Cette story est la quatrième de l'Epic 3 qui permet aux utilisateurs Pro d'automatiser les traductions:
API Keys - Génération(Story 3.1 ✅)API Keys - Révocation User(Story 3.2 ✅)API Keys - Révocation Admin(Story 3.3 ✅)- Authentification X-API-Key (cette story)
- API Versioning (Story 3.5 - backlog)
- Documentation OpenAPI (Story 3.6 - backlog)
- Webhooks (Stories 3.7-3.8 - backlog)
- Glossaires (Stories 3.9-3.10 - backlog)
- Custom Prompts (Stories 3.11-3.12 - backlog)
Valeur Business
L'authentification API est critique pour:
- Permettre l'automatisation via n8n, Zapier, scripts
- Intégration dans les pipelines CI/CD
- Accès programmatique pour les clients Pro
- Séparation claire entre web users (JWT) et automation (API key)
Dépendances
- Story 3.1 (prérequis): Génération de clés API ✅
- Story 3.2 (prérequis): Révocation utilisateur ✅
- Story 3.3 (prérequis): Révocation admin ✅
- Stories 3.7-3.12 (impact): Webhooks, glossaires, prompts utiliseront cette auth
Guardrails Développeur
❌ À NE PAS FAIRE
- NE PAS réimplémenter
get_user_by_api_key- Utiliser celle deservices/auth_service.py - NE PAS dupliquer le code de validation - Créer un middleware réutilisable
- NE PAS utiliser
HTTPExceptionavecdetailstring (utiliser JSONResponse structuré) - NE PAS oublier de tester la coexistence JWT + API key
- NE PAS utiliser camelCase dans les réponses JSON (toujours snake_case)
- NE PAS permettre l'auth API key sur les endpoints de gestion de clés (sécurité)
✅ À FAIRE
- TOUJOURS utiliser le format
{error: "CODE", message: "..."}pour les erreurs - TOUJOURS mettre à jour
last_used_atetusage_countaprès auth réussie - TOUJOURS vérifier
is_activeETexpires_at - TOUJOURS prioriser API key sur JWT si les deux sont présents
- TOUJOURS écrire des tests pour tous les cas d'erreur
- CRÉER un middleware réutilisable dans
middleware/api_key_auth.py
Code Suggéré
Fichier middleware/api_key_auth.py à créer
"""
API Key Authentication Middleware
Provides reusable dependencies for API key authentication across all endpoints.
"""
from typing import Optional
from fastapi import Header, HTTPException, Depends
from fastapi.responses import JSONResponse
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
security = HTTPBearer(auto_error=False)
class APIKeyError(Exception):
"""Exception for API key authentication errors with structured error codes."""
INVALID_API_KEY = "INVALID_API_KEY"
API_KEY_REVOKED = "API_KEY_REVOKED"
API_KEY_EXPIRED = "API_KEY_EXPIRED"
MISSING_API_KEY = "MISSING_API_KEY"
ERROR_MESSAGES = {
INVALID_API_KEY: "Clé API invalide ou non reconnue.",
API_KEY_REVOKED: "Cette clé API a été révoquée.",
API_KEY_EXPIRED: "Cette clé API a expiré.",
MISSING_API_KEY: "Clé API requise pour cet endpoint.",
}
def __init__(self, code: str, message: Optional[str] = None):
self.code = code
self.message = message or self.ERROR_MESSAGES.get(code, "Erreur d'authentification")
super().__init__(self.message)
def to_response(self, status_code: int = 401) -> JSONResponse:
return JSONResponse(
status_code=status_code,
content={
"error": self.code,
"message": self.message,
},
)
async def get_user_from_api_key(
x_api_key: Optional[str] = Header(None, alias="X-API-Key"),
) -> Optional[Any]:
"""
Get user from X-API-Key header if provided.
Returns:
User object if valid API key provided
None if no API key provided (caller should try other auth methods)
Raises:
HTTPException: 401 with structured error if API key is invalid/revoked/expired
"""
if not x_api_key:
return None
try:
from services.auth_service import get_user_by_api_key
user = get_user_by_api_key(x_api_key)
return user
except ValueError as e:
# Handle revoked/expired API keys with specific error codes
error_code = str(e)
if error_code == "API_KEY_REVOKED":
raise HTTPException(
status_code=401,
detail={
"error": "API_KEY_REVOKED",
"message": "Cette clé API a été révoquée.",
},
)
elif error_code == "API_KEY_EXPIRED":
raise HTTPException(
status_code=401,
detail={
"error": "API_KEY_EXPIRED",
"message": "Cette clé API a expiré.",
},
)
else:
# Unknown error - treat as invalid
raise HTTPException(
status_code=401,
detail={
"error": "INVALID_API_KEY",
"message": "Clé API invalide ou non reconnue.",
},
)
except Exception:
# Unexpected error - treat as invalid
raise HTTPException(
status_code=401,
detail={
"error": "INVALID_API_KEY",
"message": "Clé API invalide ou non reconnue.",
},
)
async def get_authenticated_user(
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
x_api_key: Optional[str] = Header(None, alias="X-API-Key"),
) -> Optional[Any]:
"""
Get authenticated user from API key or JWT.
Priority:
1. X-API-Key header (automation users)
2. JWT Bearer token (web users)
3. None (unauthenticated)
Returns:
User object if authenticated
None if not authenticated
"""
# Try API key first (priority for automation)
user = await get_user_from_api_key(x_api_key)
if user:
return user
# Fall back to JWT
if credentials:
try:
from routes.auth_routes import get_current_user
user = await get_current_user(credentials)
return user
except Exception:
return None
return None
async def require_authenticated_user(
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
x_api_key: Optional[str] = Header(None, alias="X-API-Key"),
) -> Any:
"""
Require authentication (API key or JWT).
Raises:
HTTPException: 401 if not authenticated
Returns:
User object (guaranteed to be authenticated)
"""
user = await get_authenticated_user(credentials, x_api_key)
if not user:
raise HTTPException(
status_code=401,
detail={
"error": "UNAUTHORIZED",
"message": "Authentification requise. Utilisez X-API-Key ou Authorization: Bearer.",
},
)
return user
async def require_api_key(
x_api_key: str = Header(..., alias="X-API-Key"),
) -> Any:
"""
Require API key authentication (no JWT fallback).
Use this for endpoints that MUST use API key (e.g., certain automation endpoints).
Raises:
HTTPException: 401 if API key is missing, invalid, revoked, or expired
Returns:
User object (guaranteed to be authenticated via API key)
"""
return await get_user_from_api_key(x_api_key)
Modification de routes/translate_routes.py
# Remplacer les fonctions locales par import du middleware
from middleware.api_key_auth import get_authenticated_user, require_authenticated_user
# Utiliser dans les endpoints
@router_v1.post("/translate")
async def translate_document_v1(
...
current_user: Optional[Any] = Depends(get_authenticated_user),
):
...
Dev Agent Record
Agent Model Used
{{agent_model_name_version}}
Debug Log References
À compléter lors de l'implémentation
Completion Notes List
- ✅ Analyse exhaustive du contexte terminée - guide complet créé pour le développeur
- ✅ Code existant analysé (auth_service.py, translate_routes.py)
- ✅ Patterns de la Story 3.3 réutilisables identifiés
- ✅ Architecture proposée pour middleware réutilisable
File List
middleware/api_key_auth.py- À CRÉER - Dépendances auth réutilisablesroutes/translate_routes.py- À MODIFIER - Utiliser nouvelle dépendancetests/test_story_3_4_api_key_authentication.py- À CRÉER
Change Log
- 2026-02-22: Story créée avec contexte complet (code existant, architecture proposée, tests suggérés)
Checklist de Validation
Avant de marquer cette story comme terminée, vérifier:
middleware/api_key_auth.pycréé avec dépendances réutilisablesget_user_from_api_keygère tous les cas d'erreur (INVALID, REVOKED, EXPIRED)get_authenticated_useressaie API key puis JWTlast_used_atetusage_countsont mis à jour après auth réussie- Les endpoints translate utilisent la nouvelle dépendance
- Les erreurs utilisent le format
{error, message} - Tous les tests passent (648 tests)
- La coexistence JWT + API key fonctionne correctement
Fichiers Modifiés/Créés
middleware/api_key_auth.py- CRÉÉ - Dépendances auth réutilisablesmiddleware/__init__.py- MODIFIÉ - Export des nouvelles dépendancesroutes/translate_routes.py- MODIFIÉ - Utilise nouvelle dépendance du middlewaretests/test_story_3_4_api_key_authentication.py- CRÉÉ - 13 tests
Résumé d'Implémentation
Ce qui a été implémenté
-
Middleware
api_key_auth.pyavec:APIKeyError- Exception structurée avec codes d'erreurget_user_from_api_key()- Valide clé API et retourne utilisateurget_authenticated_user()- Unifie API key + JWT (priorité API key)require_authenticated_user()- Dépendance qui requiert authrequire_api_key()- Dépendance qui requiert API key uniquement
-
Codes d'erreur structurés:
INVALID_API_KEY- Clé non trouvéeAPI_KEY_REVOKED- Clé révoquée (is_active=False)API_KEY_EXPIRED- Clé expirée (expires_at < now)MISSING_API_KEY- Clé manquante (si requis)
-
Tests complets (13 tests):
- Authentification réussie avec clé valide
- Erreur avec clé invalide
- Erreur avec clé révoquée
- Erreur avec clé expirée
- Coexistence JWT + API key
- Mise à jour de last_used_at et usage_count