Chapitre 2 — Authentification et autorisation#
L’authentification répond à la question qui êtes-vous ?, l’autorisation à qu’avez-vous le droit de faire ?. Ces deux mécanismes sont distincts mais complémentaires, et leurs implémentations incorrectes constituent la première source de vulnérabilités dans les APIs modernes. Ce chapitre couvre les patterns concrets : des cookies de session aux flux OAuth 2.0 complets.
API Keys#
Une API key est une chaîne de caractères secrète utilisée comme credential pour identifier et authentifier un client API (application, service, utilisateur). Simple mais puissante pour les cas d’usage M2M.
Distribution et cycle de vie#
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import APIKeyHeader
import hashlib, secrets
app = FastAPI()
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
# En production : stockage en base avec hash, pas la valeur brute
API_KEYS_DB = {
# sha256(key) -> {owner, scopes, active}
"e3b0c44298fc...": {"owner": "service-facturation", "scopes": ["invoices:read"], "active": True},
}
def get_api_key(api_key: str = Depends(api_key_header)):
if api_key is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
detail="API key manquante")
key_hash = hashlib.sha256(api_key.encode()).hexdigest()
if key_hash not in API_KEYS_DB or not API_KEYS_DB[key_hash]["active"]:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
detail="API key invalide ou révoquée")
return API_KEYS_DB[key_hash]
@app.get("/api/v1/invoices")
async def list_invoices(key_info: dict = Depends(get_api_key)):
if "invoices:read" not in key_info["scopes"]:
raise HTTPException(status_code=403, detail="Scope insuffisant")
return {"invoices": []}
Bonnes pratiques#
Ne jamais stocker la clé en clair — stocker son hash SHA-256 ou bcrypt
Génération :
secrets.token_urlsafe(32)(Python stdlib) — 32 octets = 256 bits d’entropiePréfixe lisible :
sk_live_xxxxxoupk_test_xxxxxpermet d’identifier la clé visuellement et dans les logsScopes : chaque clé doit avoir des permissions limitées au minimum nécessaire
Rotation : prévoir un mécanisme de rotation sans interruption (période de chevauchement)
Header vs query param : préférer le header
X-API-KeyouAuthorization: Bearerau query param (les query params apparaissent dans les logs serveur)
API keys dans les logs
Si l’API key est transmise en query param (?api_key=xxx), elle apparaît dans les logs d’accès Nginx/Apache, les proxies, et potentiellement les historiques de navigation. Toujours utiliser un header.
JWT en détail#
Un JSON Web Token (RFC 7519) est un token porteur auto-contenu. Il encode des claims (affirmations) signées cryptographiquement, ce qui permet leur vérification sans interroger le serveur.
Structure#
Un JWT est composé de trois parties encodées en Base64url et séparées par des points :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiJ1c2VyXzQyIiwiaXNzIjoiYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoxNzQzMDAwMDAwfQ
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header : algorithme (
alg) et type (typ)Payload : claims — iss, sub, exp, iat, jti et claims personnalisés
Signature : HMAC ou signature asymétrique du
header.payload
Claims standards#
Claim |
Description |
|---|---|
|
Issuer — émetteur du token |
|
Subject — identifiant de l’utilisateur |
|
Audience — destinataire(s) prévu(s) |
|
Expiration time — timestamp Unix |
|
Issued at — timestamp d’émission |
|
Not before — timestamp d’activation |
|
JWT ID — identifiant unique du token (révocation) |
Algorithmes : HS256 vs RS256 vs ES256#
HS256 (HMAC-SHA256) — symétrique. La même clé secrète signe et vérifie. Simple, mais tous les vérificateurs doivent connaître le secret. Approprié quand l’émetteur = le vérificateur.
RS256 (RSA-SHA256) — asymétrique. La clé privée signe, la clé publique vérifie. Les vérificateurs (Resource Servers) n’ont accès qu’à la clé publique. Approprié pour les systèmes distribués.
ES256 (ECDSA-SHA256) — asymétrique, signature plus courte que RSA, performance cryptographique supérieure. Recommandé pour les nouveaux systèmes.
Jamais alg:none
L’algorithme none désactive la signature. Certaines bibliothèques JWT l’acceptaient historiquement, permettant la falsification de tokens. Toujours rejeter explicitement les tokens avec alg: none et valider que l’algorithme correspond à celui attendu.
Validation complète#
Une validation correcte d’un JWT doit vérifier dans l’ordre :
Format (3 parties séparées par
.)Base64url décodable
Algorithme déclaré == algorithme attendu (liste blanche explicite)
Signature valide
exp> maintenantnbf<= maintenant (si présent)iss== émetteur attenduaudcontient le service courantjtinon présent dans la blacklist (si révocation activée)
Access token + Refresh token#
Le pattern standard pour les APIs OAuth :
Access token : courte durée (15 min — 1h), transmis dans chaque requête, stateless
Refresh token : longue durée (7–30 jours), stocké côté serveur (révocable), utilisé uniquement pour obtenir un nouvel access token
from fastapi import FastAPI, Depends, HTTPException
from datetime import datetime, timedelta, timezone
import hmac, hashlib, base64, json
SECRET = b"super-secret-key-32-bytes-minimum"
ALGORITHM = "HS256"
def b64url_encode(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
def b64url_decode(s: str) -> bytes:
padding = 4 - len(s) % 4
return base64.urlsafe_b64decode(s + "=" * (padding % 4))
def create_access_token(user_id: str, scopes: list[str]) -> str:
header = {"alg": "HS256", "typ": "JWT"}
now = int(datetime.now(timezone.utc).timestamp())
payload = {
"iss": "api.example.com",
"sub": user_id,
"iat": now,
"exp": now + 900, # 15 minutes
"scopes": scopes,
"jti": b64url_encode(hashlib.sha256(
f"{user_id}{now}".encode()
).digest()),
}
h = b64url_encode(json.dumps(header, separators=(",", ":")).encode())
p = b64url_encode(json.dumps(payload, separators=(",", ":")).encode())
message = f"{h}.{p}".encode()
sig = hmac.new(SECRET, message, hashlib.sha256).digest()
return f"{h}.{p}.{b64url_encode(sig)}"
OAuth 2.0#
OAuth 2.0 (RFC 6749) est un framework d’autorisation délégué. Il permet à une application tierce d’accéder à des ressources au nom d’un utilisateur, sans que l’application ne connaisse ses credentials.
Les quatre rôles#
Resource Owner : l’utilisateur qui possède les données
Client : l’application qui demande l’accès
Authorization Server (AS) : émet les tokens après authentification
Resource Server (RS) : héberge les ressources protégées, valide les tokens
Client Credentials (M2M)#
Flux pour les communications machine à machine — aucun utilisateur impliqué.
POST /oauth/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
grant_type=client_credentials&scope=reports:read+reports:write
Le service reçoit directement un access token. Pas de redirect, pas d’interaction utilisateur. Le token représente le service lui-même, pas un utilisateur.
Device Flow#
Pour les appareils sans navigateur ou à interface limitée (TV connectées, CLI, IoT) :
Le device demande un
device_codeet unuser_codeL’utilisateur va sur une URL (
verification_uri) sur un autre appareil et saisit leuser_codeLe device poll régulièrement l’AS en présentant le
device_codeUne fois l’utilisateur authentifié, le poll retourne les tokens
Refresh Token flow#
POST /oauth/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=<token>&client_id=<id>
L’AS invalide l’ancien refresh token et en émet un nouveau (refresh token rotation). Si un refresh token déjà révoqué est présenté, l’AS doit considérer toute la famille de tokens comme compromise (refresh token reuse detection).
OpenID Connect#
OpenID Connect (OIDC) est une couche d’identité construite sur OAuth 2.0. Là où OAuth gère l’autorisation (accès à des ressources), OIDC gère l’authentification (qui est l’utilisateur).
ID Token#
OIDC ajoute un ID Token (JWT) aux tokens OAuth. Il contient les informations d’identité de l’utilisateur :
{
"iss": "https://accounts.google.com",
"sub": "110169484474386276334",
"aud": "client_id_de_l_application",
"exp": 1742900400,
"iat": 1742896800,
"name": "Alice Martin",
"email": "alice@example.com",
"email_verified": true,
"nonce": "abc123"
}
Le nonce protège contre les attaques de replay.
UserInfo endpoint#
L’application peut obtenir des informations supplémentaires sur l’utilisateur :
GET /userinfo HTTP/1.1
Authorization: Bearer <access_token>
Discovery#
OIDC définit un endpoint de discovery standardisé :
GET https://accounts.example.com/.well-known/openid-configuration
La réponse JSON décrit tous les endpoints (authorization, token, userinfo, JWKS), algorithmes supportés, scopes disponibles. Cela permet aux clients de configurer automatiquement l’intégration.
Scopes OIDC#
Scope |
Claims fournis |
|---|---|
|
|
|
|
|
|
|
|
|
|
Implémentation FastAPI#
OAuth2PasswordBearer et dépendances#
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
import hmac, hashlib, base64, json
from datetime import datetime, timezone
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
SECRET = b"dev-secret-change-in-production-32b"
def verify_token(token: str) -> dict:
"""Valide un JWT HS256 simplifié."""
parts = token.split(".")
if len(parts) != 3:
raise ValueError("Format JWT invalide")
header_b64, payload_b64, sig_b64 = parts
message = f"{header_b64}.{payload_b64}".encode()
expected_sig = hmac.new(SECRET, message, hashlib.sha256).digest()
expected_b64 = base64.urlsafe_b64encode(expected_sig).rstrip(b"=").decode()
if not hmac.compare_digest(sig_b64, expected_b64):
raise ValueError("Signature invalide")
padding = 4 - len(payload_b64) % 4
payload = json.loads(base64.urlsafe_b64decode(payload_b64 + "=" * (padding % 4)))
now = int(datetime.now(timezone.utc).timestamp())
if payload.get("exp", 0) < now:
raise ValueError("Token expiré")
if payload.get("iss") != "api.example.com":
raise ValueError("Issuer invalide")
return payload
async def get_current_user(token: str = Depends(oauth2_scheme)) -> dict:
try:
payload = verify_token(token)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=str(e),
headers={"WWW-Authenticate": "Bearer"},
)
return payload
async def require_scope(scope: str):
"""Factory de dépendance pour les scopes."""
async def dependency(user: dict = Depends(get_current_user)):
if scope not in user.get("scopes", []):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Scope requis : {scope}"
)
return user
return dependency
@app.get("/api/v1/reports")
async def get_reports(user: dict = Depends(require_scope("reports:read"))):
return {"reports": [], "user": user["sub"]}
Autorisation#
L’autorisation détermine ce qu’un utilisateur authentifié est autorisé à faire. Plusieurs modèles coexistent.
RBAC — Role-Based Access Control#
Les permissions sont attachées à des rôles, les rôles sont assignés aux utilisateurs.
from enum import Enum
from fastapi import FastAPI, Depends, HTTPException
class Role(str, Enum):
ADMIN = "admin"
EDITOR = "editor"
VIEWER = "viewer"
ROLE_PERMISSIONS = {
Role.ADMIN: {"read", "write", "delete", "manage_users"},
Role.EDITOR: {"read", "write"},
Role.VIEWER: {"read"},
}
def require_permission(permission: str):
async def dependency(user: dict = Depends(get_current_user)):
role = Role(user.get("role", "viewer"))
if permission not in ROLE_PERMISSIONS.get(role, set()):
raise HTTPException(status_code=403,
detail=f"Permission '{permission}' requise")
return user
return dependency
ABAC — Attribute-Based Access Control#
Les décisions d’autorisation sont basées sur des attributs de l’utilisateur, de la ressource et du contexte. Plus flexible que RBAC mais plus complexe à implémenter.
def abac_check(user: dict, resource: dict, action: str) -> bool:
"""Exemple : accès basé sur l'appartenance organisationnelle."""
if user.get("role") == "admin":
return True
if action == "read" and resource.get("visibility") == "public":
return True
if resource.get("owner_org") == user.get("org_id"):
return action in ("read", "write")
return False
Scopes OAuth comme autorisation#
Les scopes OAuth permettent une autorisation fine au niveau de l’API :
read:users → lire les utilisateurs
write:users → créer/modifier les utilisateurs
admin:users → supprimer, changer les rôles
read:invoices → lire les factures
write:invoices → créer des factures
La convention ressource:action est lisible et extensible.
Cellules d’analyse et de visualisation#
Décodage et validation d’un JWT simulé#
import hmac, hashlib, base64, json, time
from datetime import datetime, timezone
SECRET = b"cle-secrete-demonstration-32bytes"
def b64url_encode(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
def b64url_decode(s: str) -> bytes:
padding = 4 - len(s) % 4
return base64.urlsafe_b64decode(s + "=" * (padding % 4))
def create_jwt(payload: dict, secret: bytes) -> str:
header = {"alg": "HS256", "typ": "JWT"}
h = b64url_encode(json.dumps(header, separators=(",", ":")).encode())
p = b64url_encode(json.dumps(payload, separators=(",", ":")).encode())
message = f"{h}.{p}".encode()
sig = hmac.new(secret, message, hashlib.sha256).digest()
return f"{h}.{p}.{b64url_encode(sig)}"
def validate_jwt(token: str, secret: bytes, expected_iss: str) -> tuple[bool, str, dict]:
"""Retourne (valide, message, payload)."""
parts = token.split(".")
if len(parts) != 3:
return False, "Format invalide (3 parties attendues)", {}
header_b64, payload_b64, sig_b64 = parts
# Décoder le header pour vérifier l'algorithme
try:
header = json.loads(b64url_decode(header_b64))
except Exception:
return False, "Header non décodable", {}
if header.get("alg") != "HS256":
return False, f"Algorithme inattendu : {header.get('alg')}", {}
# Vérifier la signature
message = f"{header_b64}.{payload_b64}".encode()
expected_sig = hmac.new(secret, message, hashlib.sha256).digest()
expected_b64 = b64url_encode(expected_sig)
if not hmac.compare_digest(sig_b64, expected_b64):
return False, "Signature invalide", {}
# Décoder le payload
try:
payload = json.loads(b64url_decode(payload_b64))
except Exception:
return False, "Payload non décodable", {}
# Vérifier l'expiration
now = int(datetime.now(timezone.utc).timestamp())
if "exp" in payload and payload["exp"] < now:
return False, f"Token expiré (exp={payload['exp']}, now={now})", payload
# Vérifier l'issuer
if payload.get("iss") != expected_iss:
return False, f"Issuer invalide : attendu '{expected_iss}', reçu '{payload.get('iss')}'", payload
return True, "Token valide", payload
# Création d'un token valide
now = int(datetime.now(timezone.utc).timestamp())
payload_valide = {
"iss": "api.example.com",
"sub": "user_42",
"aud": "app.example.com",
"iat": now,
"exp": now + 900,
"scopes": ["read:reports", "write:invoices"],
"jti": b64url_encode(hashlib.sha256(f"user_42{now}".encode()).digest()[:12]),
}
token = create_jwt(payload_valide, SECRET)
print("=== JWT généré ===")
parts = token.split(".")
print(f"Header : {parts[0]}")
print(f"Payload : {parts[1]}")
print(f"Signature: {parts[2][:20]}...")
print()
# Décodage manuel
header_decoded = json.loads(b64url_decode(parts[0]))
payload_decoded = json.loads(b64url_decode(parts[1]))
print("=== Header décodé ===")
print(json.dumps(header_decoded, indent=2))
print()
print("=== Payload décodé ===")
payload_display = payload_decoded.copy()
payload_display["exp_human"] = datetime.fromtimestamp(payload_decoded["exp"]).isoformat()
payload_display["iat_human"] = datetime.fromtimestamp(payload_decoded["iat"]).isoformat()
print(json.dumps(payload_display, indent=2, ensure_ascii=False))
print()
# Tests de validation
print("=== Tests de validation ===")
cas_tests = [
("Token valide", token, SECRET, "api.example.com"),
("Mauvais secret", token, b"mauvaise-cle-secrete", "api.example.com"),
("Mauvais issuer", token, SECRET, "autre.domaine.com"),
("Token malformé", "pas.un.jwt", SECRET, "api.example.com"),
("Token expiré", create_jwt({**payload_valide, "exp": now - 100}, SECRET), SECRET, "api.example.com"),
("Alg:none attack", b64url_encode(b'{"alg":"none","typ":"JWT"}') + "." +
b64url_encode(json.dumps(payload_valide).encode()) + ".", SECRET, "api.example.com"),
]
for label, tok, sec, iss in cas_tests:
valid, msg, _ = validate_jwt(tok, sec, iss)
status = "VALIDE" if valid else "REJETÉ"
print(f" [{status}] {label}: {msg}")
=== JWT généré ===
Header : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload : eyJpc3MiOiJhcGkuZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyXzQyIiwiYXVkIjoiYXBwLmV4YW1wbGUuY29tIiwiaWF0IjoxNzc0NTE4Mzg1LCJleHAiOjE3NzQ1MTkyODUsInNjb3BlcyI6WyJyZWFkOnJlcG9ydHMiLCJ3cml0ZTppbnZvaWNlcyJdLCJqdGkiOiJ5UzNCREs1d3c0WGhPMkRRIn0
Signature: RoqqcMJGKI9eckpOlrAI...
=== Header décodé ===
{
"alg": "HS256",
"typ": "JWT"
}
=== Payload décodé ===
{
"iss": "api.example.com",
"sub": "user_42",
"aud": "app.example.com",
"iat": 1774518385,
"exp": 1774519285,
"scopes": [
"read:reports",
"write:invoices"
],
"jti": "yS3BDK5ww4XhO2DQ",
"exp_human": "2026-03-26T11:01:25",
"iat_human": "2026-03-26T10:46:25"
}
=== Tests de validation ===
[VALIDE] Token valide: Token valide
[REJETÉ] Mauvais secret: Signature invalide
[REJETÉ] Mauvais issuer: Issuer invalide : attendu 'autre.domaine.com', reçu 'api.example.com'
[REJETÉ] Token malformé: Header non décodable
[REJETÉ] Token expiré: Token expiré (exp=1774518285, now=1774518385)
[REJETÉ] Alg:none attack: Algorithme inattendu : none
Diagramme Client Credentials (M2M)#
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
fig, ax = plt.subplots(figsize=(11, 6))
ax.set_xlim(0, 11)
ax.set_ylim(0, 9)
ax.axis("off")
fig.patch.set_facecolor("#f8f9fa")
ax.set_facecolor("#f8f9fa")
actors_m2m = {"Service A\n(Client)", "Authorization Server", "Service B\n(Resource Server)"}
positions = {"Service A\n(Client)": 1.5, "Authorization Server": 5.5, "Service B\n(Resource Server)": 9.5}
cols_m2m = {"Service A\n(Client)": "#4CAF50", "Authorization Server": "#FF9800", "Service B\n(Resource Server)": "#9C27B0"}
for name, x in positions.items():
c = cols_m2m[name]
ax.text(x, 8.5, name, ha="center", va="center", fontsize=10,
fontweight="bold", color="white",
bbox=dict(boxstyle="round,pad=0.4", facecolor=c, edgecolor="none"))
ax.plot([x, x], [1.0, 8.2], color=c, lw=1.2, linestyle="--", alpha=0.4)
def m2m_arrow(ax, x1, x2, y, label, color, sub=""):
ax.annotate("", xy=(x2, y), xytext=(x1, y),
arrowprops=dict(arrowstyle="-|>", color=color, lw=1.5, mutation_scale=13))
mid = (x1 + x2) / 2
ax.text(mid, y + 0.22, label, ha="center", va="bottom", fontsize=9, color=color)
if sub:
ax.text(mid, y - 0.2, sub, ha="center", va="top", fontsize=7.5,
color="#666666", style="italic")
# Étape 1
ax.text(3.5, 7.5, "① Authentification du service (pas d'utilisateur)",
ha="center", fontsize=8.5, color="#444",
bbox=dict(boxstyle="round,pad=0.25", facecolor="#FFF9C4", edgecolor="#E0C000"))
m2m_arrow(ax, 1.5, 5.5, 6.9,
"POST /oauth/token",
cols_m2m["Service A\n(Client)"],
sub="grant_type=client_credentials&scope=reports:read (Basic Auth: client_id:secret)")
m2m_arrow(ax, 5.5, 1.5, 5.9,
"access_token (JWT, exp=3600s)",
cols_m2m["Authorization Server"])
# Étape 2
ax.text(5.5, 5.2, "② Appel API avec le token — valide pour toute la durée",
ha="center", fontsize=8.5, color="#444",
bbox=dict(boxstyle="round,pad=0.25", facecolor="#FFF9C4", edgecolor="#E0C000"))
m2m_arrow(ax, 1.5, 9.5, 4.6,
"GET /api/reports",
cols_m2m["Service A\n(Client)"],
sub="Authorization: Bearer <access_token>")
m2m_arrow(ax, 9.5, 1.5, 3.6,
"200 OK — données",
cols_m2m["Service B\n(Resource Server)"])
# Étape 3 (renouvellement)
ax.text(3.5, 3.0, "③ Renouvellement automatique avant expiration",
ha="center", fontsize=8.5, color="#444",
bbox=dict(boxstyle="round,pad=0.25", facecolor="#FFF9C4", edgecolor="#E0C000"))
m2m_arrow(ax, 1.5, 5.5, 2.4,
"POST /oauth/token (nouveau cycle)",
cols_m2m["Service A\n(Client)"])
m2m_arrow(ax, 5.5, 1.5, 1.5,
"Nouveau access_token",
cols_m2m["Authorization Server"])
ax.set_title("Flux OAuth 2.0 Client Credentials — communication M2M",
fontsize=12, fontweight="bold", pad=8)
plt.savefig("oauth_client_credentials.png", dpi=120, bbox_inches="tight",
facecolor="#f8f9fa")
plt.show()
Comparaison des mécanismes d’authentification#
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
mecanismes = ["Cookie\nde session", "API Key", "JWT\nHS256", "JWT\nRS256/ES256",
"OAuth 2.0\nAuth Code + PKCE", "OAuth 2.0\nClient Credentials", "mTLS"]
criteres = ["Sécurité", "Simplicité\nimplémentation", "Scalabilité\nhorizontale",
"Révocation\naisée", "Adapté\nnavigateur", "Adapté\nM2M"]
# Scores 1-5 (5 = excellent)
scores = np.array([
# Sécu Simplic Scal Révoc Nav M2M
[3, 5, 3, 5, 5, 1], # Session cookie
[3, 5, 5, 4, 2, 5], # API Key
[4, 3, 5, 2, 3, 4], # JWT HS256
[5, 2, 5, 3, 3, 4], # JWT RS256
[5, 2, 5, 4, 5, 2], # OAuth Auth Code + PKCE
[5, 3, 5, 3, 1, 5], # OAuth Client Credentials
[5, 1, 5, 5, 1, 5], # mTLS
])
fig, ax = plt.subplots(figsize=(12, 5.5))
x = np.arange(len(criteres))
n = len(mecanismes)
width = 0.11
palette = sns.color_palette("muted", n)
for i, (mec, score_row) in enumerate(zip(mecanismes, scores)):
offset = (i - n/2 + 0.5) * width
bars = ax.bar(x + offset, score_row, width * 0.88,
label=mec.replace("\n", " "),
color=palette[i], alpha=0.85, edgecolor="white")
ax.set_xticks(x)
ax.set_xticklabels(criteres, fontsize=9.5)
ax.set_yticks([1, 2, 3, 4, 5])
ax.set_yticklabels(["1\nMinimal", "2\nFaible", "3\nMoyen", "4\nBon", "5\nExcellent"],
fontsize=8.5)
ax.set_ylim(0, 6.2)
ax.set_ylabel("Score (1–5)", fontsize=10)
ax.set_title("Comparaison des mécanismes d'authentification pour les APIs",
fontsize=12, fontweight="bold", pad=10)
ax.legend(loc="upper right", fontsize=8, ncol=2,
bbox_to_anchor=(1.0, 1.0))
ax.grid(axis="y", alpha=0.4)
plt.savefig("auth_mechanisms_comparison.png", dpi=120, bbox_inches="tight")
plt.show()
print("\nRésumé par cas d'usage :")
print(" Application web SPA → OAuth 2.0 Authorization Code + PKCE")
print(" Application mobile native → OAuth 2.0 Authorization Code + PKCE")
print(" Service à service (M2M) → OAuth 2.0 Client Credentials ou mTLS")
print(" API simple (usage interne)→ API Key + TLS")
print(" Application legacy web → Cookie de session + SameSite=Lax")
print(" Haute sécurité (finance) → mTLS + JWT")
Résumé par cas d'usage :
Application web SPA → OAuth 2.0 Authorization Code + PKCE
Application mobile native → OAuth 2.0 Authorization Code + PKCE
Service à service (M2M) → OAuth 2.0 Client Credentials ou mTLS
API simple (usage interne)→ API Key + TLS
Application legacy web → Cookie de session + SameSite=Lax
Haute sécurité (finance) → mTLS + JWT
Résumé#
Ce chapitre a couvert les mécanismes d’authentification et d’autorisation des APIs modernes :
Sessions et cookies — adaptées aux applications web classiques avec serveur stateful. Les attributs HttpOnly, Secure, et SameSite=Lax sont le minimum de sécurité. Limitées pour les APIs multi-clients.
API Keys — simples et efficaces pour le M2M. Ne jamais stocker en clair (hash SHA-256), utiliser les headers plutôt que les query params, assigner des scopes minimaux.
JWT — tokens auto-contenus avec expiration. La validation doit être complète : format, algorithme explicite (liste blanche), signature, expiration, issuer, audience. Utiliser RS256 ou ES256 pour les architectures distribuées. Le pattern access + refresh token est le standard.
OAuth 2.0 — Authorization Code + PKCE pour les clients publics (SPA, mobile) ; Client Credentials pour le M2M ; Device Flow pour les appareils sans navigateur. La rotation des refresh tokens est obligatoire.
OpenID Connect — ajoute la couche d’identité (ID Token, UserInfo) sur OAuth 2.0. Le discovery via .well-known/openid-configuration simplifie l’intégration.
Autorisation — RBAC pour la simplicité, ABAC pour la finesse. La vérification de l’appartenance d’une ressource à l’utilisateur (BOLA) est la protection la plus critique et la plus souvent omise.