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

9.8 KiB
Raw Blame History

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 refreshPOST /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

  • 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 bloc router_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). Si None (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_token et create_refresh_token.
    • 1.7 Retourner 200 avec format {"data": {"access_token", "refresh_token", "token_type": "bearer"}, "meta": {}}.
  • Task 2: Tests (AC: 14)

    • 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 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]
    {
      "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é.
    {
      "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 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).