Retour à l'accueil

Autorisation JWT FastAPI : code étape par étape

Guide pour implémenter l'authentification JWT dans FastAPI pour développeurs middle/senior. Couvre la structure des tokens, schémas Pydantic, module de sécurité, dépendances et endpoints protégés avec code complet.

JWT dans FastAPI : code de connexion + routes protégées
Advertisement 728x90

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) :

Google AdInline article slot
{"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 :

Google AdInline article slot
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 :

Google AdInline article slot
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

Advertisement 728x90

Lire ensuite