Mise en œuvre de l'authentification JWT avec FastAPI : Guide complet pour développeurs
JWT (JSON Web Token) est un jeton compact pour transmettre des revendications entre parties. Le jeton se compose de trois parties encodées en Base64 : en-tête, charge utile et signature, séparées par des points.
En-tête contient l'algorithme de signature (par exemple, HS256) et le type de jeton (JWT) :
{"alg": "HS256", "typ": "JWT"}
Charge utile stocke les revendications : sub (identifiant utilisateur), iat (date d'émission), exp (date d'expiration) :
{"sub": "alex", "name": "Alex Tomchok", "iat": 1516239022, "exp": 1516242622}
Signature est calculée comme HMAC-SHA256 de (en-tête.charge utile + clé secrète). Le serveur vérifie la signature lors de la validation : toute modification de la charge utile invalide le jeton. Les données sont lisibles mais protégées contre la modification.
Les jetons d'accès ont une durée de vie courte (15–30 minutes), tandis que les jetons de rafraîchissement sont à longue durée (jours/mois). Cet exemple utilise uniquement des jetons d'accès.
Configuration du projet et dépendances
Créez la structure :
fastapi-jwt-auth/
├── app/
│ ├── main.py
│ ├── core/
│ │ ├── config.py
│ │ └── security.py
│ ├── schemas/
│ │ ├── token.py
│ │ └── user.py
│ ├── api/
│ │ └── v1/
│ │ ├── endpoints/
│ │ │ ├── auth.py
│ │ │ └── users.py
│ │ └── dependencies.py
│ └── services/
│ └── user_service.py
└── requirements.txt
requirements.txt :
fastapi==0.104.1
uvicorn[standard]==0.24.0
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-multipart==0.0.6
pydantic[email]==2.5.0
Installez : pip install -r requirements.txt.
Configuration et schémas Pydantic
Dans core/config.py, définissez les paramètres avec Pydantic :
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
SECRET_KEY: str = "votre-clé-super-secrète-changez-ceci-en-production"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
class Config:
env_file = ".env"
settings = Settings()
Schémas dans schemas/user.py :
from pydantic import BaseModel, EmailStr
class UserBase(BaseModel):
username: str
email: EmailStr
class UserCreate(UserBase):
password: str
class User(UserBase):
id: int
is_active: bool = True
class Config:
from_attributes = True
class UserInDB(User):
hashed_password: str
schemas/token.py :
from pydantic import BaseModel
class Token(BaseModel):
access_token: str
token_type: str = "bearer"
class TokenData(BaseModel):
username: str | None = None
Sécurité : Hachage et opérations JWT
Dans core/security.py, implémentez le hachage de mot de passe (bcrypt) et les fonctions JWT :
from datetime import datetime, timedelta
from jose import JWTError, jwt
from passlib.context import CryptContext
from .config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def create_access_token(data: dict) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
def decode_access_token(token: str) -> dict:
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
return payload
except JWTError:
return None
Service utilisateur et base de données fictive
services/user_service.py avec stockage en mémoire :
from app.schemas.user import UserInDB
from app.core.security import verify_password, get_password_hash
fake_users_db = {
"alex": {
"id": 1,
"username": "alex",
"email": "[email protected]",
"hashed_password": get_password_hash("secret123"),
"is_active": True,
}
}
def get_user_by_username(username: str) -> UserInDB | None:
if username in fake_users_db:
return UserInDB(**fake_users_db[username])
return None
def authenticate_user(username: str, password: str) -> UserInDB | None:
user = get_user_by_username(username)
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return user
Dépendances pour l'autorisation
Dans api/v1/dependencies.py, utilisez OAuth2PasswordBearer :
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from app.core.security import decode_access_token
from app.services.user_service import get_user_by_username
from app.schemas.token import TokenData
from app.schemas.user import User
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Impossible de valider les identifiants",
headers={"WWW-Authenticate": "Bearer"},
)
payload = decode_access_token(token)
if payload is None:
raise credentials_exception
username: str = payload.get("sub")
if username is None:
raise credentials_exception
user = get_user_by_username(username)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
if not current_user.is_active:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Utilisateur inactif")
return current_user
Points de terminaison : Connexion et routes protégées
api/v1/endpoints/auth.py :
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from app.schemas.token import Token
from app.services.user_service import authenticate_user
from app.core.security import create_access_token
router = APIRouter(prefix="/auth", tags=["authentication"])
@router.post("/login", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Nom d'utilisateur ou mot de passe incorrect",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = create_access_token(data={"sub": user.username})
return Token(access_token=access_token, token_type="bearer")
api/v1/endpoints/users.py :
from fastapi import APIRouter, Depends
from app.schemas.user import User
from app.api.v1.dependencies import get_current_active_user
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
return current_user
Connectez les routeurs dans main.py et exécutez uvicorn app.main:app.
Points clés
- La charge utile JWT n'est pas chiffrée, seulement signée — utilisez uniquement des données non sensibles.
- Stockez SECRET_KEY dans des variables d'environnement, générez-la avec 32+ caractères.
- En production, ajoutez des jetons de rafraîchissement et une base de données (SQLAlchemy + Alembic).
- Les dépendances FastAPI permettent de réutiliser la logique d'autorisation sans duplication.
- Testez les points de terminaison via Swagger UI :
/docs.
— Editorial Team
Aucun commentaire pour le moment.