Files
office_translator/_bmad-output/implementation-artifacts/1-5-refresh-token.md
Sepehr Ramezani 26bd096a06 feat: production deployment - full update with providers, admin, glossaries, pricing, tests
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>
2026-04-25 15:01:47 +02:00

176 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Story 1.5: Refresh Token
Status: done
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
## 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
1. **AC1: Endpoint refresh**`POST /api/v1/auth/refresh` existe et accepte un corps JSON `{"refresh_token": "<string>"}`.
2. **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).
3. **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).
4. **AC4: Corps manquant ou invalide** — Requête sans corps ou sans champ `refresh_token` valide retourne 400 avec `{"error": "INVALID_REQUEST", "message": "..."}`.
## Tasks / Subtasks
- [x] **Task 1: Endpoint POST /api/v1/auth/refresh** (AC: 1, 2, 3, 4)
- [x] 1.1 Dans `routes/auth_routes.py`, ajouter une route `@router_v1.post("/refresh")` dans le bloc `router_v1`.
- [x] 1.2 Parser le corps JSON pour extraire `refresh_token` (obligatoire). Si absent ou invalide → 400 INVALID_REQUEST.
- [x] 1.3 Appeler `verify_token(refresh_token)`. Si `None` (expiré, invalide, révoqué) → 401 TOKEN_EXPIRED.
- [x] 1.4 Vérifier `payload.get("type") == "refresh"`. Sinon → 401 TOKEN_EXPIRED.
- [x] 1.5 Récupérer l'utilisateur via `get_user_by_id(payload["sub"])`. Si absent → 401 TOKEN_EXPIRED.
- [x] 1.6 Générer nouvel access_token (15 min) et nouvel refresh_token (7 jours) via `create_access_token` et `create_refresh_token`.
- [x] 1.7 Retourner 200 avec format `{"data": {"access_token", "refresh_token", "token_type": "bearer"}, "meta": {}}`.
- [x] **Task 2: Tests** (AC: 14)
- [x] 2.1 Créer ou étendre `tests/test_auth_refresh.py` (ou fichier dédié refresh v1).
- [x] 2.2 Test : refresh avec token valide → 200 + nouvel access_token et refresh_token.
- [x] 2.3 Test : refresh avec token expiré → 401 TOKEN_EXPIRED.
- [x] 2.4 Test : refresh avec token révoqué (après logout) → 401.
- [x] 2.5 Test : refresh sans corps ou sans refresh_token → 400 INVALID_REQUEST.
- [x] 2.6 Test : nouvel access_token a bien 15 min dexpiry (optionnel, vérification payload JWT).
## Dev Notes
### Contexte brownfield
Le projet a déjà :
- **API v1 auth** : `router_v1` dans `routes/auth_routes.py` avec prefix `/api/v1/auth` ; routes existantes : `/register`, `/login`, `/logout`. Il **ny a pas** dendpoint `/refresh` sous v1 (le refresh existant est sous lancien routeur `/api/auth/refresh`). La story demande dajouter **uniquement** `POST /api/v1/auth/refresh` pour alignement avec register/login/logout.
- **Auth service** : `create_access_token`, `create_refresh_token`, `verify_token`, `get_user_by_id`, blocklist JTI (révocation) dans `services/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à `None` pour 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]
```json
{
"data": {
"access_token": "<jwt>",
"refresh_token": "<jwt>",
"token_type": "bearer"
},
"meta": {}
}
```
- **Format erreur (401)** — Pas de champ `data` ; code `TOKEN_EXPIRED` pour token invalide/expiré/révoqué.
```json
{
"error": "TOKEN_EXPIRED",
"message": "Token invalide ou expiré"
}
```
- **Format erreur (400)** — Corps manquant ou invalide.
```json
{
"error": "INVALID_REQUEST",
"message": "Refresh token requis"
}
```
### Patterns à réutiliser (Story 1.3 / 1.4)
- Même style dendpoint que `login_v1` : `request.json()` pour le corps, `JSONResponse` avec `data`/`meta`, messages derreur en français.
- Utiliser `create_access_token(user.id, tier=user.plan.value, expires_delta=timedelta(minutes=15))` et `create_refresh_token(user.id, expires_delta=timedelta(days=7))` comme dans `login_v1`.
- Vérifier `verify_token(refresh_token)` puis `payload.get("type") == "refresh"` et `get_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 nexpose 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 sans `data`, avec `error` et `message` (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 avec `await request.json()` et gestion dexception pour 400.
- **PyJWT** : Déjà utilisé dans `auth_service` ; pas de nouvelle dépendance.
### File structure
- Route : `routes/auth_routes.py` — ajout dune seule fonction `refresh_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 dinté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 dans `verify_token`), `routes/auth_routes.py` (endpoint logout v1), `tests/test_auth_logout.py`.
- **Patterns** : Réponse 200 avec `{"data": {"message": "..."}, "meta": {}}` ; 401 avec `TOKEN_MISSING` / `TOKEN_INVALID` ; extraction Bearer et vérification via `verify_token`. Pour refresh, réutiliser `verify_token` (qui retourne déjà `None` pour 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/refresh` avec 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_v1` est monté dans `main.py` ; sassurer 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/refresh` dans `routes/auth_routes.py` (fonction `refresh_v1`) : parsing du corps JSON, validation du `refresh_token`, vérification via `verify_token` et `type == "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 AC1AC4 (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` — guard `isinstance(body, dict)` pour éviter 500 sur corps JSON non-objet ; (2) test `test_non_object_json_body_returns_400` ajouté ; (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 (AC1AC4).