Python
Fastapi
Jwt
Tests
Configurer l'authentification JWT et les tests d'intégration sur FastAPI avec une base claire pour un vrai projet backend.
Objectif de l'article
Configurer l'authentification JWT et les tests d'intégration sur FastAPI avec une base claire pour un vrai projet backend.
A retenir: JWT (JSON Web Tokens) est le standard moderne pour l'authentification sans état. Combiné avec FastAPI et une suite de tests solide, vous créez une API sécurisée et maintenable en production.
Concepts clés
Avant de coder, comprenez ces concepts fondamentaux pour FastAPI et JWT :
- JWT (JSON Web Token) : Token stateless composé de 3 parties (header.payload.signature)
- Bearer Token : Format standard pour transporter le JWT dans l'en-tête Authorization
- Refresh Token : Token de longue durée pour renouveler l'accès sans re-login
- FastAPI : Framework moderne et rapide basé sur Starlette et Pydantic
- Scopes : Permissions pour contrôler les accès (admin, user, read, write)
- Tests d'intégration : Tests end-to-end qui vérifient le flux complet
- Pytest : Framework de test Python pour automatiser les tests
Implémentation
Étape 1 : Installation des dépendances
pip install fastapi uvicorn python-jose[cryptography] passlib[bcrypt] pytest httpx python-multipart
Étape 2 : Créer les modèles Pydantic
# models.py
from pydantic import BaseModel
from typing import Optional
class TokenResponse(BaseModel):
access_token: str
refresh_token: Optional[str] = None
token_type: str = "bearer"
class UserCreate(BaseModel):
email: str
password: str
username: str
class UserResponse(BaseModel):
id: int
email: str
username: str
class Config:
from_attributes = True
class TokenPayload(BaseModel):
sub: str # subject (user id)
scopes: list = []
exp: int # expiration time
Étape 3 : Configurer JWT et sécurité
# security.py
from datetime import datetime, timedelta
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthCredentials
import os
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-in-prod")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
security = HTTPBearer()
def hash_password(password: str) > str:
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) > bool:
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(data: dict):
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
async def get_current_user(credentials: HTTPAuthCredentials = Depends(security)):
token = credentials.credentials
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id: str = payload.get("sub")
if user_id is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
except JWTError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return {"user_id": user_id}
Étape 4 : Routes d'authentification
# auth.py
from fastapi import APIRouter, HTTPException, Depends
from security import hash_password, verify_password, create_access_token, get_current_user
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/register", response_model=UserResponse)
async def register(user_data: UserCreate):
hashed_password = hash_password(user_data.password)
# Sauvegarder en BD
return {"id": 1, "email": user_data.email, "username": user_data.username}
@router.post("/login", response_model=TokenResponse)
async def login(email: str, password: str):
# Vérifier utilisateur en BD
access_token = create_access_token(data={"sub": "user_id"})
return {
"access_token": access_token,
"token_type": "bearer"
}
@router.get("/me", response_model=UserResponse)
async def get_me(current_user = Depends(get_current_user)):
# Récupérer utilisateur depuis BD
return {"id": 1, "email": "user@example.com", "username": "user"}
Étape 5 : Application principale
# main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from auth import router as auth_router
app = FastAPI(title="API FastAPI JWT")
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(auth_router)
@app.get("/health")
async def health():
return {"status": "ok"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Tests d'intégration
Configuration de Pytest
# conftest.py
import pytest
from fastapi.testclient import TestClient
from main import app
@pytest.fixture
def client():
return TestClient(app)
@pytest.fixture
def test_user_data():
return {
"email": "test@example.com",
"password": "testpass123",
"username": "testuser"
}
Tests du flux d'authentification
# test_auth.py
import pytest
def test_register_success(client, test_user_data):
response = client.post("/auth/register", json=test_user_data)
assert response.status_code == 200
assert response.json()["email"] == test_user_data["email"]
def test_login_success(client, test_user_data):
client.post("/auth/register", json=test_user_data)
response = client.post(
"/auth/login",
params={
"email": test_user_data["email"],
"password": test_user_data["password"]
}
)
assert response.status_code == 200
assert "access_token" in response.json()
def test_login_invalid_credentials(client):
response = client.post(
"/auth/login",
params={"email": "wrong@example.com", "password": "wrongpass"}
)
assert response.status_code == 401
def test_get_current_user(client, test_user_data):
client.post("/auth/register", json=test_user_data)
login_response = client.post(
"/auth/login",
params={
"email": test_user_data["email"],
"password": test_user_data["password"]
}
)
token = login_response.json()["access_token"]
response = client.get(
"/auth/me",
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
def test_unauthorized_access(client):
response = client.get("/auth/me", headers={"Authorization": "Bearer invalid_token"})
assert response.status_code == 401
Lancer les tests
pytest -v
pytest --cov=.
pytest -k "test_login" -v
Intégration en production
Bonnes pratiques de sécurité
- SECRET_KEY : Clé forte et unique en production (variables d'env)
- HTTPS obligatoire : Les tokens doivent transiter en HTTPS
- Token short-lived : Access tokens de courte durée (15-30 min)
- CORS restrictif : Ne pas accepter toutes les origines
- Rate limiting : Limiter les tentatives de login
- Logging : Enregistrer les tentatives suspectes
Déploiement avec Gunicorn
pip install gunicorn
gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
Configuration Nginx
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Variables d'environnement (.env)
SECRET_KEY=your-very-secret-key-change-this
DATABASE_URL=postgresql://user:password@localhost/dbname
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
Checklist de production
- ✅ SECRET_KEY fort et unique
- ✅ HTTPS configuré
- ✅ Rate limiting activé
- ✅ Logging et monitoring
- ✅ Tests passent 100%
- ✅ CORS restrictif
- ✅ Headers de sécurité
- ✅ Backups réguliers
- ✅ Monitoring des erreurs
Prochaines étapes
- Ajouter OAuth2 (Google, GitHub)
- Implémenter 2FA
- Ajouter des logs d'audit
- Mettre en place un blacklist de tokens
- Intégrer un monitoring (Prometheus, Grafana)
- Ajouter des tests de performance