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>
9.8 KiB
9.8 KiB
Story 1.5: Refresh Token
Status: done
Story
As a logged-in user, I want to refresh my access token using my refresh token, so that I can stay logged in without re-entering credentials.
Acceptance Criteria
- AC1: Endpoint refresh —
POST /api/v1/auth/refreshexiste et accepte un corps JSON{"refresh_token": "<string>"}. - AC2: Nouvel access token — Pour un refresh token valide, la réponse est 200 avec
{"data": {"access_token": "...", "refresh_token": "...", "token_type": "bearer"}, "meta": {}}. Le nouvel access_token a une durée de vie de 15 minutes (NFR6). - AC3: Refresh token invalide/expiré — Si le refresh token est invalide, expiré ou révoqué, la réponse est 401 avec
{"error": "TOKEN_EXPIRED", "message": "Token invalide ou expiré"}(ou message équivalent en français). - AC4: Corps manquant ou invalide — Requête sans corps ou sans champ
refresh_tokenvalide retourne 400 avec{"error": "INVALID_REQUEST", "message": "..."}.
Tasks / Subtasks
-
Task 1: Endpoint POST /api/v1/auth/refresh (AC: 1, 2, 3, 4)
- 1.1 Dans
routes/auth_routes.py, ajouter une route@router_v1.post("/refresh")dans le blocrouter_v1. - 1.2 Parser le corps JSON pour extraire
refresh_token(obligatoire). Si absent ou invalide → 400 INVALID_REQUEST. - 1.3 Appeler
verify_token(refresh_token). SiNone(expiré, invalide, révoqué) → 401 TOKEN_EXPIRED. - 1.4 Vérifier
payload.get("type") == "refresh". Sinon → 401 TOKEN_EXPIRED. - 1.5 Récupérer l'utilisateur via
get_user_by_id(payload["sub"]). Si absent → 401 TOKEN_EXPIRED. - 1.6 Générer nouvel access_token (15 min) et nouvel refresh_token (7 jours) via
create_access_tokenetcreate_refresh_token. - 1.7 Retourner 200 avec format
{"data": {"access_token", "refresh_token", "token_type": "bearer"}, "meta": {}}.
- 1.1 Dans
-
Task 2: Tests (AC: 1–4)
- 2.1 Créer ou étendre
tests/test_auth_refresh.py(ou fichier dédié refresh v1). - 2.2 Test : refresh avec token valide → 200 + nouvel access_token et refresh_token.
- 2.3 Test : refresh avec token expiré → 401 TOKEN_EXPIRED.
- 2.4 Test : refresh avec token révoqué (après logout) → 401.
- 2.5 Test : refresh sans corps ou sans refresh_token → 400 INVALID_REQUEST.
- 2.6 Test : nouvel access_token a bien 15 min d’expiry (optionnel, vérification payload JWT).
- 2.1 Créer ou étendre
Dev Notes
Contexte brownfield
Le projet a déjà :
- API v1 auth :
router_v1dansroutes/auth_routes.pyavec prefix/api/v1/auth; routes existantes :/register,/login,/logout. Il n’y a pas d’endpoint/refreshsous v1 (le refresh existant est sous l’ancien routeur/api/auth/refresh). La story demande d’ajouter uniquementPOST /api/v1/auth/refreshpour alignement avec register/login/logout. - Auth service :
create_access_token,create_refresh_token,verify_token,get_user_by_id, blocklist JTI (révocation) dansservices/auth_service.py. Expiry access 15 min, refresh 7 jours déjà en place pour login v1. - Story 1.4 : Logout révoque le JTI du refresh token ;
verify_token()retourne déjàNonepour un token révoqué. Aucun changement de schéma ou de blocklist nécessaire pour la story 1.5.
Architecture Compliance
- Format succès (200) — [Source: architecture.md]
{ "data": { "access_token": "<jwt>", "refresh_token": "<jwt>", "token_type": "bearer" }, "meta": {} } - Format erreur (401) — Pas de champ
data; codeTOKEN_EXPIREDpour token invalide/expiré/révoqué.{ "error": "TOKEN_EXPIRED", "message": "Token invalide ou expiré" } - Format erreur (400) — Corps manquant ou invalide.
{ "error": "INVALID_REQUEST", "message": "Refresh token requis" }
Patterns à réutiliser (Story 1.3 / 1.4)
- Même style d’endpoint que
login_v1:request.json()pour le corps,JSONResponseavecdata/meta, messages d’erreur en français. - Utiliser
create_access_token(user.id, tier=user.plan.value, expires_delta=timedelta(minutes=15))etcreate_refresh_token(user.id, expires_delta=timedelta(days=7))comme danslogin_v1. - Vérifier
verify_token(refresh_token)puispayload.get("type") == "refresh"etget_user_by_id(payload["sub"]).
Fichiers à modifier / créer
| Fichier | Action |
|---|---|
routes/auth_routes.py |
Ajouter @router_v1.post("/refresh") et logique (parser body, verify_token, type refresh, get_user_by_id, create tokens, retour 200). |
tests/test_auth_refresh.py (ou équivalent) |
Créer/étendre avec tests v1 pour POST /api/v1/auth/refresh (valide, expiré, révoqué, corps manquant). |
Fichiers à ne pas modifier
services/auth_service.py— Pas de changement nécessaire (verify_token, create_access_token, create_refresh_token et blocklist déjà en place).database/— Aucune migration.- Endpoints legacy
/api/auth/*— Hors scope ; on n’expose que/api/v1/auth/refresh.
Références
- [Source: _bmad-output/planning-artifacts/epics.md#Story 1.5]
- [Source: _bmad-output/planning-artifacts/architecture.md#Authentication & Security]
- [Source: _bmad-output/planning-artifacts/architecture.md#API Response Formats]
- [Source: _bmad-output/implementation-artifacts/1-4-logout-utilisateur.md — Patterns JTI / verify_token]
- [Source: _bmad-output/implementation-artifacts/1-3-login-utilisateur-jwt.md — Format login v1]
Developer Context (Guardrails)
Technical requirements
- Backend : FastAPI, Python 3.11+. Endpoint sous
router_v1(prefix/api/v1/auth). - JWT : PyJWT ; access 15 min, refresh 7 jours ;
verify_token()gère déjà la blocklist JTI (Story 1.4). - Réponses : Succès avec
data+meta; erreurs sansdata, avecerroretmessage(snake_case, français).
Architecture compliance
- Conventions API : JSON snake_case, format succès/erreur comme ci-dessus.
- Auth : Pas de stockage de tokens en clair ; refresh token utilisé une seule fois par échange (rotations optionnelles ultérieures).
Library / framework
- FastAPI :
Request,JSONResponse; parser body avecawait request.json()et gestion d’exception pour 400. - PyJWT : Déjà utilisé dans
auth_service; pas de nouvelle dépendance.
File structure
- Route :
routes/auth_routes.py— ajout d’une seule fonctionrefresh_v1(ou nom cohérent) et@router_v1.post("/refresh"). - Tests :
tests/test_auth_refresh.py(nouveau fichier recommandé pour v1) ou section dédiée dans un fichier de tests auth existant.
Testing requirements
- Tests d’intégration (client FastAPI) pour POST /api/v1/auth/refresh : cas valide (200 + structure data), token expiré (401), token révoqué après logout (401), corps manquant (400). Réutiliser fixtures utilisateur/tokens des tests login/logout si possible.
Previous Story Intelligence (1-4 Logout)
- Fichiers modifiés :
services/auth_service.py(JTI, blocklist,revoke_token_jti,is_token_revoked, vérification dansverify_token),routes/auth_routes.py(endpoint logout v1),tests/test_auth_logout.py. - Patterns : Réponse 200 avec
{"data": {"message": "..."}, "meta": {}}; 401 avecTOKEN_MISSING/TOKEN_INVALID; extraction Bearer et vérification viaverify_token. Pour refresh, réutiliserverify_token(qui retourne déjàNonepour token révoqué) et ne pas dupliquer la logique de blocklist. - Tests : Fixture avec réinitialisation de la blocklist (
_revoked_jtis) pour éviter fuites entre tests ; test AC2 dans 1-4 appelle déjàPOST /api/auth/refreshavec refresh token révoqué et attend 401. Pour 1.5, ajouter des tests explicites pour POST /api/v1/auth/refresh.
Project Context Reference
- Structure : Backend à la racine (ou sous backend/) :
main.py,routes/,services/,database/,tests/. Pas de changement de structure requis. - Montage des routes :
router_v1est monté dansmain.py; s’assurer que le nouvel endpoint est bien exposé sous/api/v1/auth/refresh.
Story Completion Status
- Status : done
- Note : Code review (adversarial) : correctifs appliqués (guard body dict + test body non-objet, statut aligné). Fichiers à committer : routes/auth_routes.py, tests/test_auth_refresh.py.
Dev Agent Record
Agent Model Used
{{agent_model_name_version}}
Debug Log References
Completion Notes List
- Implémentation de
POST /api/v1/auth/refreshdansroutes/auth_routes.py(fonctionrefresh_v1) : parsing du corps JSON, validation durefresh_token, vérification viaverify_tokenettype == "refresh", récupération utilisateur, génération de nouveaux tokens (access 15 min, refresh 7 jours), réponse 200 avec format data/meta. Erreurs 400 INVALID_REQUEST (corps manquant ou refresh_token absent/invalide), 401 TOKEN_EXPIRED (token expiré, invalide, révoqué ou utilisateur absent). - Tests dans
tests/test_auth_refresh.py: 13 tests couvrant AC1–AC4 (succès 200 + structure, tokens différents, expiry 15 min ; token expiré 401 ; token révoqué après logout 401 ; corps manquant / champ absent / chaîne vide / type invalide → 400). Suite complète : 81 tests passent, aucune régression. - [Code review 2026-02-20] Correctifs : (1)
refresh_v1— guardisinstance(body, dict)pour éviter 500 sur corps JSON non-objet ; (2) testtest_non_object_json_body_returns_400ajouté ; (3) statut story aligné (review → done).
File List
- routes/auth_routes.py (modifié — ajout endpoint refresh_v1)
- tests/test_auth_refresh.py (créé)
Change Log
- 2026-02-20 : Implémentation story 1.5 — endpoint POST /api/v1/auth/refresh et tests (AC1–AC4).