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>
176 lines
9.8 KiB
Markdown
176 lines
9.8 KiB
Markdown
# 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: 1–4)
|
||
- [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 d’expiry (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 **n’y a pas** d’endpoint `/refresh` sous v1 (le refresh existant est sous l’ancien routeur `/api/auth/refresh`). La story demande d’ajouter **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 d’endpoint que `login_v1` : `request.json()` pour le corps, `JSONResponse` avec `data`/`meta`, messages d’erreur 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 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 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 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 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 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 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` ; 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/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 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` — 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 (AC1–AC4).
|