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 :

SolutionScénarios applicablesDescription
Authentification HTTP de baseService interne simpleLe 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é APIAppel inter-servicesTransmettez la clé via les en-têtes de requête, les paramètres de requête ou les cookies
OAuth2 + JWTApplication avec front-end/back-end séparésL'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 :

  1. Le client envoie le nom d'utilisateur et le mot de passe à/tokenPoint de terminaison
  2. Le serveur vérifie les identifiants et renvoie un jeton d'accès JWT
  3. Le client inclut le jeton dans les requêtes suivantes (Authorization: Bearer <token>)
  4. 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 datetime import datetime, timedelta
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 :

FonctionDescription
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érationFonctionDescription
Créer un jetonjwt.encode()Encoder les données dans une chaîne JWT
Analyser le jetonjwt.decode()Analyser et vérifier la chaîne JWT
Définir l'expiration"exp": expireTemps d'expiration du jeton
Stocker l'identifiant utilisateur"sub": usernameLe 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 » :

  1. Cliquez« Authorize »
  2. Saisissez le nom d'utilisateur et le mot de passe (par ex.alice / secret)
  3. Cliquez« Authorize »Obtenir le jeton
  4. 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
autres extensions