---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# 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.

## Sessions et cookies

Le mécanisme de session est le plus ancien pattern d'authentification pour les applications web. Le serveur crée une session côté serveur, lui attribue un identifiant opaque, et le transmet au client via un cookie.

### Le cycle de vie d'une session

1. L'utilisateur s'authentifie (POST avec credentials)
2. Le serveur vérifie les credentials, crée une session en base ou en mémoire partagée (Redis)
3. Le serveur répond avec `Set-Cookie: session_id=<valeur_opaque>; ...`
4. Le client renvoie automatiquement le cookie à chaque requête suivante
5. Le serveur valide le cookie, retrouve la session, identifie l'utilisateur

### Attributs de sécurité des cookies

```
Set-Cookie: session_id=4f7b2a9c...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
```

| Attribut | Effet |
|----------|-------|
| `HttpOnly` | Le cookie est inaccessible depuis JavaScript — protection contre le vol par XSS |
| `Secure` | Transmis uniquement sur HTTPS |
| `SameSite=Strict` | Le cookie n'est pas envoyé lors de navigations cross-site — protection CSRF forte |
| `SameSite=Lax` | Envoyé pour les navigations top-level (liens), pas pour les requêtes embedded — équilibre courant |
| `SameSite=None; Secure` | Cross-site autorisé — uniquement pour des cas légitimes (iframes, OAuth) |
| `Max-Age=N` | Durée de vie en secondes |
| `Domain` | Si omis, le cookie est limité au host exact |

### Limitations pour les APIs

Les cookies de session présentent plusieurs limites importantes pour les APIs modernes :

- **Statefulness** : le serveur doit stocker l'état de session (scalabilité horizontale complexe sans session partagée via Redis)
- **CSRF** : nécessite une protection explicite (token CSRF ou `SameSite`)
- **Clients non-navigateurs** : les clients mobiles, CLI, et services M2M ne gèrent pas naturellement les cookies
- **Cross-domain** : la politique same-site complique les architectures multi-domaines

```{admonition} Sessions vs tokens
:class: note
Les sessions sont appropriées pour les applications web traditionnelles où le serveur contrôle le client (SPAs, applications classiques). Pour les APIs consommées par des clients variés (mobile, tiers, M2M), les tokens porteurs (JWT, API keys) sont plus adaptés.
```

## 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

```python
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'entropie
- **Préfixe lisible** : `sk_live_xxxxx` ou `pk_test_xxxxx` permet d'identifier la clé visuellement et dans les logs
- **Scopes** : 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-Key` ou `Authorization: Bearer` au query param (les query params apparaissent dans les logs serveur)

```{admonition} API keys dans les logs
:class: warning
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 |
|-------|-------------|
| `iss` | Issuer — émetteur du token |
| `sub` | Subject — identifiant de l'utilisateur |
| `aud` | Audience — destinataire(s) prévu(s) |
| `exp` | Expiration time — timestamp Unix |
| `iat` | Issued at — timestamp d'émission |
| `nbf` | Not before — timestamp d'activation |
| `jti` | 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.

```{admonition} Jamais alg:none
:class: important
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 :

1. Format (3 parties séparées par `.`)
2. Base64url décodable
3. Algorithme déclaré == algorithme attendu (liste blanche explicite)
4. Signature valide
5. `exp` > maintenant
6. `nbf` <= maintenant (si présent)
7. `iss` == émetteur attendu
8. `aud` contient le service courant
9. `jti` non 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

```python
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

### Authorization Code + PKCE

Le flux le plus sécurisé, utilisé pour les SPAs, applications mobiles, et applications web.

**PKCE** (*Proof Key for Code Exchange*, RFC 7636) protège contre l'interception du code d'autorisation. Le client génère un `code_verifier` aléatoire et envoie son hash `code_challenge` au démarrage du flux.

Étapes :
1. Client génère `code_verifier` (43–128 caractères aléatoires)
2. Client calcule `code_challenge = BASE64URL(SHA256(code_verifier))`
3. Redirect vers l'AS avec `code_challenge` et `code_challenge_method=S256`
4. Utilisateur s'authentifie sur l'AS
5. AS redirige vers le client avec le `code` d'autorisation
6. Client échange le code contre des tokens en présentant le `code_verifier`
7. L'AS vérifie que `SHA256(code_verifier) == code_challenge` stocké

```python
import secrets, hashlib, base64, urllib.parse

def generate_pkce() -> tuple[str, str]:
    """Génère un code_verifier et son code_challenge PKCE."""
    verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b"=").decode()
    digest = hashlib.sha256(verifier.encode()).digest()
    challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
    return verifier, challenge

def build_authorization_url(
    as_url: str,
    client_id: str,
    redirect_uri: str,
    scopes: list[str],
    code_challenge: str,
    state: str
) -> str:
    params = {
        "response_type": "code",
        "client_id": client_id,
        "redirect_uri": redirect_uri,
        "scope": " ".join(scopes),
        "code_challenge": code_challenge,
        "code_challenge_method": "S256",
        "state": state,
    }
    return f"{as_url}/authorize?{urllib.parse.urlencode(params)}"
```

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

1. Le device demande un `device_code` et un `user_code`
2. L'utilisateur va sur une URL (`verification_uri`) sur un autre appareil et saisit le `user_code`
3. Le device poll régulièrement l'AS en présentant le `device_code`
4. Une 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 :

```json
{
  "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 |
|-------|---------------|
| `openid` | `sub` (obligatoire pour OIDC) |
| `profile` | `name`, `given_name`, `family_name`, `locale`, ... |
| `email` | `email`, `email_verified` |
| `address` | `address` (structuré) |
| `phone` | `phone_number`, `phone_number_verified` |

## Implémentation FastAPI

### OAuth2PasswordBearer et dépendances

```python
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.

```python
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.

```python
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
```

### Broken Object Level Authorization (BOLA)

La vulnérabilité la plus fréquente dans les APIs : un utilisateur accède aux ressources d'un autre en changeant l'identifiant dans l'URL.

```python
@app.get("/api/v1/invoices/{invoice_id}")
async def get_invoice(invoice_id: int, user: dict = Depends(get_current_user)):
    invoice = db.get_invoice(invoice_id)
    if not invoice:
        raise HTTPException(status_code=404)
    # CRITIQUE : vérifier que l'invoice appartient bien à l'utilisateur
    if invoice["user_id"] != user["sub"] and "admin" not in user.get("roles", []):
        raise HTTPException(status_code=403, detail="Accès non autorisé")
    return invoice
```

### 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é

```{code-cell} python3
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}")
```

### Diagramme Authorization Code + PKCE

```{code-cell} python3
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=(13, 10))
ax.set_xlim(0, 13)
ax.set_ylim(0, 15)
ax.axis("off")
fig.patch.set_facecolor("#f8f9fa")
ax.set_facecolor("#f8f9fa")

actors = {
    "Utilisateur": 1.5,
    "SPA / App mobile": 4.0,
    "Authorization Server": 7.5,
    "Resource Server": 11.5,
}

actor_colors = {
    "Utilisateur": "#5C85D6",
    "SPA / App mobile": "#4CAF50",
    "Authorization Server": "#FF9800",
    "Resource Server": "#9C27B0",
}

for name, x in actors.items():
    color = actor_colors[name]
    ax.text(x, 14.5, name, ha="center", va="center", fontsize=9.5,
            fontweight="bold",
            bbox=dict(boxstyle="round,pad=0.4", facecolor=color,
                      edgecolor="none", alpha=0.85), color="white")
    ax.plot([x, x], [0.5, 14.2], color=color, lw=1.0,
            linestyle="--", alpha=0.4)

def flow_arrow(ax, x1, x2, y, label, color="#444", sub="", dashed=False):
    style = "-->" if dashed else "-|>"
    ax.annotate("", xy=(x2, y), xytext=(x1, y),
                arrowprops=dict(arrowstyle="-|>", color=color, lw=1.4,
                                linestyle="dashed" if dashed else "solid",
                                mutation_scale=12))
    mid = (x1 + x2) / 2
    ax.text(mid, y + 0.22, label, ha="center", va="bottom",
            fontsize=8, color=color, fontweight="semibold")
    if sub:
        ax.text(mid, y - 0.18, sub, ha="center", va="top",
                fontsize=7, color="#777777", style="italic")

def step_label(ax, y, n, text):
    ax.text(0.1, y, f"({n})", fontsize=8, color="#888888", va="center")

# Étapes PKCE
step_label(ax, 13.7, 1, "")
ax.text(6.5, 13.75, "① PKCE : génération de code_verifier + code_challenge",
        ha="center", fontsize=8.5, color="#555",
        bbox=dict(boxstyle="round,pad=0.2", facecolor="#FFF9C4", edgecolor="#F0D060"))

flow_arrow(ax, 1.5, 4.0, 13.0, "Clique 'Se connecter'", actor_colors["Utilisateur"])
flow_arrow(ax, 4.0, 7.5, 12.2,
           "Redirect → /authorize",
           actor_colors["SPA / App mobile"],
           sub="?code_challenge=SHA256(verifier)&code_challenge_method=S256&scope=openid profile")

flow_arrow(ax, 7.5, 1.5, 11.2,
           "Page de connexion",
           actor_colors["Authorization Server"])
flow_arrow(ax, 1.5, 7.5, 10.4,
           "Saisit login + mot de passe",
           actor_colors["Utilisateur"])

ax.text(6.5, 9.85, "② Authentification réussie — AS génère le code d'autorisation",
        ha="center", fontsize=8.5, color="#555",
        bbox=dict(boxstyle="round,pad=0.2", facecolor="#FFF9C4", edgecolor="#F0D060"))

flow_arrow(ax, 7.5, 4.0, 9.3,
           "Redirect → redirect_uri",
           actor_colors["Authorization Server"],
           sub="?code=AUTH_CODE&state=xyz")

ax.text(6.5, 8.75, "③ Échange code → tokens (présentation du code_verifier)",
        ha="center", fontsize=8.5, color="#555",
        bbox=dict(boxstyle="round,pad=0.2", facecolor="#FFF9C4", edgecolor="#F0D060"))

flow_arrow(ax, 4.0, 7.5, 8.2,
           "POST /token",
           actor_colors["SPA / App mobile"],
           sub="code=AUTH_CODE + code_verifier (AS vérifie SHA256(verifier)==challenge)")

flow_arrow(ax, 7.5, 4.0, 7.2,
           "access_token + id_token + refresh_token",
           actor_colors["Authorization Server"])

ax.text(6.5, 6.65, "④ Utilisation de l'access token",
        ha="center", fontsize=8.5, color="#555",
        bbox=dict(boxstyle="round,pad=0.2", facecolor="#FFF9C4", edgecolor="#F0D060"))

flow_arrow(ax, 4.0, 11.5, 6.1,
           "GET /api/resource",
           actor_colors["SPA / App mobile"],
           sub="Authorization: Bearer <access_token>")
flow_arrow(ax, 11.5, 4.0, 5.1,
           "200 OK — données protégées",
           actor_colors["Resource Server"])

ax.text(6.5, 4.5, "⑤ Renouvellement silencieux via refresh token",
        ha="center", fontsize=8.5, color="#555",
        bbox=dict(boxstyle="round,pad=0.2", facecolor="#FFF9C4", edgecolor="#F0D060"))

flow_arrow(ax, 4.0, 7.5, 3.9,
           "POST /token (grant_type=refresh_token)",
           actor_colors["SPA / App mobile"])
flow_arrow(ax, 7.5, 4.0, 3.0,
           "Nouveaux access_token + refresh_token (rotation)",
           actor_colors["Authorization Server"])

ax.set_title("Flux OAuth 2.0 Authorization Code + PKCE",
             fontsize=13, fontweight="bold", pad=8)
plt.savefig("oauth_pkce_flow.png", dpi=120, bbox_inches="tight",
            facecolor="#f8f9fa")
plt.show()
```

### Diagramme Client Credentials (M2M)

```{code-cell} python3
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

```{code-cell} python3
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é

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.
