# 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 1. **AC1: Endpoint refresh** — `POST /api/v1/auth/refresh` existe et accepte un corps JSON `{"refresh_token": ""}`. 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": "", "refresh_token": "", "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).