Sécurité et authentification FastAPI
FastAPI dispose d'outils de sécurité intégrés qui prennent en charge les méthodes courantes d'authentification et d'autorisation telles qu'OAuth2, les jetons JWT et les clés API. Cette section explique comment mettre en œuvre l'authentification des utilisateurs basée sur OAuth2 + JWT.
Aperçu de la sécurité et de l'authentification
Schémas de sécurité pris en charge par FastAPI :
| Solution | Scénarios applicables | Description |
|---|---|---|
| Authentification HTTP de base | Service interne simple | Le nom d'utilisateur et le mot de passe sont encodés dans l'en-tête de requête, ce qui offre une sécurité moindre |
| Clé API | Appel inter-services | Transmettez la clé via les en-têtes de requête, les paramètres de requête ou les cookies |
| OAuth2 + JWT | Application avec front-end/back-end séparés | L'approche la plus courante, sécurisée et flexible |
Mode mot de passe OAuth2 + JWT
Il s'agit du schéma d'authentification le plus couramment utilisé pour les applications à front-end et back-end séparés. Flux :
- Le client envoie le nom d'utilisateur et le mot de passe à
/tokenPoint de terminaison - Le serveur vérifie les identifiants et renvoie un jeton d'accès JWT
- Le client inclut le jeton dans les requêtes suivantes (
Authorization: Bearer <token>) - Le serveur vérifie le jeton pour identifier l'utilisateur
1. Installer les dépendances
pip install "python-jose[cryptography]" passlib[bcrypt]
2. Exemple complet
Exemple
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel
# ===== Configuration =====
SECRET_KEY = "your-secret-key-keep-it-secret" # Utilisez des variables d'environnement en production
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# ===== Hachage des mots de passe =====
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# ===== Schéma OAuth2 =====
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# ===== Modèles de données =====
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: str | None = None
class User(BaseModel):
username: str
email: str | None = None
full_name: str | None = None
disabled: bool | None = None
class UserInDB(User):
hashed_password: str
# ===== Base de données fictive =====
fake_users_db = {
"alice": {
"username": "alice",
"full_name": "Alice Wonderson",
"email": "[email protected]",
"hashed_password": pwd_context.hash("secret"), # Mot de passe: secret
"disabled": False,
}
}
# ===== Fonctions utilitaires =====
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Valider le mot de passe"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""Générer le hachage du mot de passe"""
return pwd_context.hash(password)
def get_user(db: dict, username: str) -> UserInDB | None:
"""Récupérer l'utilisateur depuis la base de données"""
if username in db:
return UserInDB(**db[username])
return None
def authenticate_user(db: dict, username: str, password: str):
"""Vérifier les identifiants de l'utilisateur"""
user = get_user(db, username)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user
def create_access_token(data: dict, expires_delta: timedelta | None = Aucune):
"""Créer un jeton d'accès JWT"""
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
"""Obtenir l'utilisateur actuel à partir du token (fonction de dépendance)"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=Impossible de valider les identifiants,
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = get_user(fake_users_db, username)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(
current_user: Annotated[User, Depends(get_current_user)],
):
"""Obtenir l'utilisateur actif actuel"""
if current_user.disabled:
raise HTTPException(status_code=400, detail=L'utilisateur a été désactivé)
return current_user
# ===== Routes =====
app = FastAPI()
@app.post("/token")
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
"""Connexion pour obtenir un jeton"""
user = authenticate_user(fake_users_db, form_data.username, form_data.mot de passe)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Nom d'utilisateur ou mot de passe invalide",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/users/me")
async def read_users_me(
current_user: Annotated[User, Depends(get_current_active_user)],
):
"""Obtenir les informations de l'utilisateur actuel (authentification requise)"""
return current_user
@app.get("/users/me/items")
async def read_own_items(
current_user: Annotated[User, Depends(get_current_active_user)],
):
"""Obtenir les éléments de l'utilisateur actuel (authentification requise)"""
return [{"item_id": "Foo", "owner": current_user.username}]
Explication du code
Hachage du mot de passe
UtilisationpasslibL'algorithme bcrypt hache les mots de passe pour garantir que les mots de passe en clair ne sont pas stockés dans la base de données :
| Fonction | Description |
|---|---|
pwd_context.hash(password) | Convertir le mot de passe en clair en une valeur de hachage |
pwd_context.verify(plain, hashed) | Vérifier si le mot de passe en clair correspond à la valeur de hachage |
Jeton JWT
JWT (JSON Web Token) est un format de jeton sécurisé qui contient les informations de l'utilisateur et l'heure d'expiration :
| Opération | Fonction | Description |
|---|---|---|
| Créer un jeton | jwt.encode() | Encoder les données dans une chaîne JWT |
| Analyser le jeton | jwt.decode() | Analyser et vérifier la chaîne JWT |
| Définir l'expiration | "exp": expire | Temps d'expiration du jeton |
| Stocker l'identifiant utilisateur | "sub": username | Le sujet du jeton (généralement le nom d'utilisateur) |
OAuth2PasswordBearer
Indiquer à FastAPI à partir deAuthorization: Bearer <token>Obtenir le jeton depuis l'en-tête de requête :
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
tokenUrl="token"Spécifie le chemin du point de terminaison pour que le client obtienne le jeton, qui apparaîtra dans la documentation de l'API.
SECRET_KEYIl doit être gardé secret et être suffisamment complexe ; en environnement de production, des variables d'environnement doivent être utilisées pour le stockage. Si la clé secrète est divulguée, les attaquants peuvent falsifier des jetons pour n'importe quel utilisateur.
Tester à l'aide de la documentation de l'API
Après avoir configuré OAuth2, l'interface Swagger UI affichera un bouton « Authorize » :
- Cliquez« Authorize »
- Saisissez le nom d'utilisateur et le mot de passe (par ex.
alice/secret) - Cliquez« Authorize »Obtenir le jeton
- Toutes les requêtes suivantes porteront automatiquement le jeton
Résumé
- OAuth2 + JWT est le mécanisme d'authentification le plus couramment utilisé pour les applications où le front-end et le back-end sont séparés
- Les mots de passe doivent être hachés pour le stockage et ne doivent pas être stockés en clair
- Les jetons JWT contiennent des identifiants utilisateur et une date d'expiration
- En utilisant l'injection de dépendances (
Depends) pour implémenter la réutilisation de la logique d'authentification - Dans les environnements de production, la SECRET_KEY doit rester secrète