# 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).
