Files
office_translator/_bmad-output/implementation-artifacts/1-3-login-utilisateur-jwt.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

11 KiB
Raw Blame History

Story 1.3: Login Utilisateur (JWT)

Status: done

Story

As a registered user, I want to login with email and password, so that I can access my dashboard and translation features.

Acceptance Criteria

  1. AC1: Login Success - When correct email + password submitted, receive access_token (15min expiry) and refresh_token (7 days expiry)
  2. AC2: JWT Signing - Tokens are JWT signed with SECRET_KEY from environment variable
  3. AC3: Invalid Password - Returns 401 with {error: "INVALID_CREDENTIALS", message: "..."}
  4. AC4: User Not Found - Returns 401 with {error: "INVALID_CREDENTIALS", message: "..."} (security: same error as wrong password to prevent email enumeration)
  5. AC5: API Versioning - Endpoint at /api/v1/auth/login (not legacy /api/auth/login)

Tasks / Subtasks

  • Task 1: Create Login Endpoint (AC: 1, 2, 5)

    • 1.1 Create POST /api/v1/auth/login endpoint in routes/auth_routes.py
    • 1.2 Accept UserLogin schema with email and password fields
    • 1.3 Return TokenResponse with access_token, refresh_token, and token_type: "bearer"
    • 1.4 Generate access token with 15min expiry using PyJWT
    • 1.5 Generate refresh token with 7 days expiry using PyJWT
  • Task 2: Implement JWT Token Generation (AC: 2)

    • 2.1 Create or update create_access_token() function in services/auth_service.py
    • 2.2 Create or update create_refresh_token() function in services/auth_service.py
    • 2.3 Use SECRET_KEY from core/config.py (loaded from env)
    • 2.4 Include sub (user_id) and exp claims in tokens
    • 2.5 Optionally include tier claim for quick access
  • Task 3: Implement Credential Validation (AC: 1, 3, 4)

    • 3.1 Fetch user by email using existing get_user_by_email() from services/auth_service.py
    • 3.2 Verify password using existing verify_password() function (passlib[bcrypt])
    • 3.3 Return standardized error for user not found: 401 INVALID_CREDENTIALS (security: same as wrong password to prevent email enumeration)
    • 3.4 Return standardized error for wrong password: 401 INVALID_CREDENTIALS
    • 3.5 Use same error message pattern as Story 1.2 (French messages)
  • Task 4: Standardize Error Responses (AC: 3, 4)

    • 4.1 User not found → 401 with {error: "INVALID_CREDENTIALS", message: "Email ou mot de passe incorrect"} (security: prevents email enumeration)
    • 4.2 Invalid password → 401 with {error: "INVALID_CREDENTIALS", message: "Mot de passe incorrect"}
    • 4.3 Invalid email format → 400 with {error: "INVALID_EMAIL", message: "Format d'email invalide"} (Pydantic validation)
    • 4.4 Use JSONResponse pattern from Story 1.2 for error responses
  • Task 5: Add Tests (AC: 1-5)

    • 5.1 Create tests/test_auth_login.py
    • 5.2 Test: successful login returns 200 + tokens with correct expiry
    • 5.3 Test: invalid password returns 401 INVALID_CREDENTIALS
    • 5.4 Test: non-existent user returns 401 INVALID_CREDENTIALS (security: same error as wrong password)
    • 5.5 Test: invalid email format returns 400 INVALID_EMAIL
    • 5.6 Test: tokens contain correct user_id in sub claim
    • 5.7 Test: tokens are signed and verifiable with SECRET_KEY

Dev Notes

🚨 BROWNFIELD CONTEXT - CRITICAL

This is REFACTORING, not greenfield. Existing code to leverage:

File Current State Action
routes/auth_routes.py Has /api/auth/login (legacy) + /api/v1/auth/register Add /api/v1/auth/login
services/auth_service.py Has verify_password(), get_user_by_email(), hash_password() Reuse functions
core/config.py Should have SECRET_KEY Verify/ensure exists
database/models.py User model with id, email, hashed_password, tier Already complete (Story 1.1)

Architecture Compliance

API Response Format (from architecture.md):

Success (200):

{
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "token_type": "bearer"
  },
  "meta": {}
}

Error (401):

{
  "error": "INVALID_CREDENTIALS",
  "message": "Mot de passe incorrect"
}

⚠️ NO data field in error responses!

Token Structure (from architecture.md:183-184):

Access Token: 15min expiry
Refresh Token: 7 days expiry
Algorithm: HS256 (PyJWT default)
Claims: sub (user_id), exp (expiry), optionally tier

Naming (from architecture.md:276-289):

  • DB/API: snake_case (user_id, access_token)
  • Python: snake_case variables/functions, PascalCase classes
  • Endpoint: /api/v1/auth/login

Existing Code Patterns to Follow (from Story 1.2)

Router pattern (established in Story 1.2):

# routes/auth_routes.py
router_v1 = APIRouter(prefix="/api/v1/auth")

@router_v1.post("/login")
async def login_v1(user_login: UserLogin):
    # Implementation

Error response pattern (from Story 1.2):

from fastapi.responses import JSONResponse

# For errors, use JSONResponse directly (not HTTPException)
return JSONResponse(
    status_code=401,
    content={
        "error": "INVALID_CREDENTIALS",
        "message": "Mot de passe incorrect"
    }
)

Password verification (services/auth_service.py):

def verify_password(plain_password: str, hashed_password: str) -> bool:
    if PASSLIB_AVAILABLE:
        return pwd_context.verify(plain_password, hashed_password)
    else:
        # Fallback SHA256 (dev only)

User lookup (services/auth_service.py:180-200):

def get_user_by_email(email: str) -> Optional[User]:
    # Already exists, reuse

Dependencies (Already Installed)

Package Version Purpose
PyJWT 2.8.0 JWT token generation
passlib[bcrypt] 1.7.4 Password verification
pydantic 2.x Request/response schemas
pytest 7.x Testing

Files to Modify

File Action Notes
routes/auth_routes.py MODIFY Add /api/v1/auth/login endpoint
services/auth_service.py MODIFY Add token generation functions if not present
schemas/auth_schemas.py or inline CREATE/MODIFY UserLogin schema
core/config.py VERIFY Ensure SECRET_KEY setting exists
tests/test_auth_login.py CREATE New test file

Files NOT to Touch

  • database/models.py - User model already complete (Story 1.1)
  • database/connection.py - Already setup
  • alembic/ - No schema changes needed
  • Frontend files - Not in scope

Token Generation Code Reference

import jwt
from datetime import datetime, timedelta, timezone
from core.config import settings

def create_access_token(user_id: str, tier: str = "free") -> str:
    payload = {
        "sub": user_id,
        "tier": tier,
        "exp": datetime.now(timezone.utc) + timedelta(minutes=15),
        "type": "access"
    }
    return jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")

def create_refresh_token(user_id: str) -> str:
    payload = {
        "sub": user_id,
        "exp": datetime.now(timezone.utc) + timedelta(days=7),
        "type": "refresh"
    }
    return jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")

Testing Commands

# Run tests
pytest tests/test_auth_login.py -v

# Test endpoint manually
curl -X POST http://localhost:8000/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "test@example.com", "password": "Password123!"}'

Security Considerations (from NFR6)

  • Access token expires in 15 minutes
  • Refresh token expires in 7 days
  • SECRET_KEY must come from environment (never hardcoded)
  • Password verification uses bcrypt (not plaintext comparison)
  • Error messages should not reveal whether email exists vs password is wrong (but AC4 requires separate error - use same timing to prevent enumeration)

Project Structure Notes

  • Backend uses snake_case for files, variables, functions
  • PascalCase for classes
  • Follow existing patterns in routes/ and services/
  • Tests in tests/ directory following pytest conventions

References

  • [Source: _bmad-output/planning-artifacts/epics.md#Story 1.3]
  • [Source: _bmad-output/planning-artifacts/architecture.md#Authentication & Security]
  • [Source: _bmad-output/planning-artifacts/architecture.md#API Response Formats]
  • [Source: _bmad-output/planning-artifacts/prd.md#FR18]
  • [Source: _bmad-output/planning-artifacts/prd.md#NFR6]
  • [Source: _bmad-output/implementation-artifacts/1-2-inscription-utilisateur.md - Previous story patterns]

Dev Agent Record

Agent Model Used

claude-4.6-sonnet-medium-thinking (Cursor)

Debug Log References

Aucun blocage rencontré. Implémentation directe suivant les patterns établis en Story 1.2.

Completion Notes List

  • Endpoint créé : POST /api/v1/auth/login dans router_v1 de routes/auth_routes.py, suivant exactement le pattern de register_v1 (parse JSON manuel + PydanticValidationError catch).
  • Token generation : create_access_token() appelé avec expires_delta=timedelta(minutes=15) ; create_refresh_token() mis à jour pour accepter un paramètre expires_delta optionnel, appelé avec timedelta(days=7). Les endpoints legacy continuent d'utiliser leurs valeurs par défaut (24h / 30j) sans impact.
  • Gestion d'erreurs : INVALID_CREDENTIALS (401) pour email inconnu ET mot de passe incorrect (sécurité : évite l'énumération d'emails), INVALID_EMAIL (400) si format email invalide (Pydantic), INVALID_REQUEST (400) pour JSON malformé ou champs manquants. Tous les messages sont en français conformément à la Story 1.2.
  • Format de réponse : {"data": {"access_token": ..., "refresh_token": ..., "token_type": "bearer"}, "meta": {}} conforme à l'architecture.
  • Token tier claim : create_access_token() inclut le claim tier pour accès rapide au plan utilisateur.
  • Tests : 24 tests dans tests/test_auth_login.py couvrant AC1AC5 + tier claim + algorithme HS256.
  • Cycle RED-GREEN-REFACTOR : tests écrits et confirmés échouants (404) avant l'implémentation.

File List

  • routes/auth_routes.py — modifié : ajout import timedelta et verify_password, ajout endpoint login_v1, passage du tier à create_access_token(), fix sécurité anti-énumération email
  • services/auth_service.py — modifié : paramètre expires_delta ajouté à create_refresh_token(), paramètre tier ajouté à create_access_token()
  • tests/test_auth_login.py — créé : 24 tests couvrant AC1AC5, tier claim, algorithme HS256

Change Log

  • 2026-02-20 : Code review — fix tier claim manquant, fix anti-énumération email (USER_NOT_FOUND → INVALID_CREDENTIALS), ajout tests tier claim et HS256
  • 2026-02-20 : Implémentation Story 1.3 — endpoint POST /api/v1/auth/login avec JWT (15min/7j), validation des credentials, gestion d'erreurs standardisée, 24 tests ajoutés.