Skip to content

🛠️ Module 4 — Mise en pratique en Python

Bonne lecture et bon apprentissage ! Junior TSAFACK – 12/09/2026 ⏱️ Temps de lecture estimé : 12 minutes


Assez de théorie ! 🧪 Nous allons construire un véritable serveur d’autorisation OAuth 2.0 avec Flask, puis jouer le rôle du navigateur et du Client pour exécuter le flux complet — tout le code de ce module a été réellement exécuté (environnement Anaconda dédié), les sorties affichées sont authentiques.

Terminal window
conda create -n oauth-course python=3.11
conda activate oauth-course
pip install flask requests

Un mini serveur d’autorisation avec 3 endpoints, exactement ceux vus au Module 3 : /authorize, /token, et un /userinfo qui joue le rôle de Resource Server.

import base64
import hashlib
import secrets
import time
from flask import Flask, jsonify, redirect, request
app = Flask(__name__)
REGISTERED_CLIENTS = {"demo-client": "s3cr3t-partage-uniquement-serveur-a-serveur"}
AUTH_CODES = {} # code -> {client_id, expires_at, code_challenge}
ACCESS_TOKENS = {} # token -> {client_id}
@app.route("/authorize")
def authorize():
client_id = request.args.get("client_id")
redirect_uri = request.args.get("redirect_uri")
state = request.args.get("state")
code_challenge = request.args.get("code_challenge")
if client_id not in REGISTERED_CLIENTS:
return jsonify({"error": "invalid_client"}), 400
# Dans une vraie appli, l'utilisateur verrait ici un ecran de consentement.
code = secrets.token_urlsafe(16)
AUTH_CODES[code] = {
"client_id": client_id,
"expires_at": time.time() + 60,
"code_challenge": code_challenge,
}
return redirect(f"{redirect_uri}?code={code}&state={state}")
@app.route("/token", methods=["POST"])
def token():
data = request.form
code = data.get("code")
client_id = data.get("client_id")
client_secret = data.get("client_secret")
code_verifier = data.get("code_verifier")
if REGISTERED_CLIENTS.get(client_id) != client_secret:
return jsonify({"error": "invalid_client"}), 401
entry = AUTH_CODES.get(code)
if not entry or entry["client_id"] != client_id:
return jsonify({"error": "invalid_grant", "error_description": "code inconnu"}), 400
if time.time() > entry["expires_at"]:
return jsonify({"error": "invalid_grant", "error_description": "code expire"}), 400
# Verification PKCE
if entry["code_challenge"]:
computed = base64.urlsafe_b64encode(
hashlib.sha256((code_verifier or "").encode()).digest()
).rstrip(b"=").decode()
if computed != entry["code_challenge"]:
return jsonify({"error": "invalid_grant", "error_description": "PKCE verifier invalide"}), 400
del AUTH_CODES[code] # usage unique
access_token = secrets.token_urlsafe(24)
ACCESS_TOKENS[access_token] = {"client_id": client_id}
return jsonify({"access_token": access_token, "token_type": "Bearer", "expires_in": 3600})
@app.route("/userinfo")
def userinfo():
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return jsonify({"error": "invalid_token"}), 401
access_token = auth_header.removeprefix("Bearer ")
if access_token not in ACCESS_TOKENS:
return jsonify({"error": "invalid_token"}), 401
return jsonify({"sub": "user-42", "name": "Junior TSAFACK", "email": "[email protected]"})

🎬 Le flux complet, joué par un vrai client

Section titled “🎬 Le flux complet, joué par un vrai client”

Le serveur Flask tourne dans un thread ; le reste du script joue tour à tour le rôle du navigateur puis du Client backend.

import threading
from urllib.parse import parse_qs, urlparse
import requests
threading.Thread(target=lambda: app.run(port=5001), daemon=True).start()
time.sleep(1)
BASE = "http://localhost:5001"
CLIENT_ID = "demo-client"
CLIENT_SECRET = "s3cr3t-partage-uniquement-serveur-a-serveur"
REDIRECT_URI = "http://localhost:9999/callback" # n'existe pas : on lit juste l'URL de redirection
# --- PKCE : genere AVANT le premier appel ---
code_verifier = secrets.token_urlsafe(48)
code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest()).rstrip(b"=").decode()
print(f"code_verifier (garde secret) : {code_verifier}")
print(f"code_challenge (envoye en clair) : {code_challenge}")
# --- ETAPE 1 : le navigateur est redirige vers /authorize ---
resp = requests.get(f"{BASE}/authorize", params={
"client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI, "state": "xyz123",
"response_type": "code", "code_challenge": code_challenge, "code_challenge_method": "S256",
}, allow_redirects=False)
print(f"GET /authorize -> HTTP {resp.status_code}")
print(f"Location: {resp.headers['Location']}")
query = parse_qs(urlparse(resp.headers["Location"]).query)
auth_code, state = query["code"][0], query["state"][0]
# --- ETAPE 2 : le CLIENT (backend) echange le code ---
resp = requests.post(f"{BASE}/token", data={
"grant_type": "authorization_code", "code": auth_code,
"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "code_verifier": code_verifier,
})
print(f"POST /token -> HTTP {resp.status_code}")
print(resp.json())
access_token = resp.json()["access_token"]
# --- ETAPE 3 : le CLIENT lit le profil ---
resp = requests.get(f"{BASE}/userinfo", headers={"Authorization": f"Bearer {access_token}"})
print(f"GET /userinfo -> HTTP {resp.status_code}")
print(resp.json())

Sortie réellement obtenue :

code_verifier (garde secret par le client) : dqDeL6an5EHJY82EtaZp3a5PqYcMknRf5pNK9rLCIWGL5nwg5PxmWw_gkvBV0DWe
code_challenge (envoye en clair a /authorize) : VkVNlNnjznyt9lNZ5WsDjCljMdbkDnDdLFq7-Ciqdvs
GET /authorize -> HTTP 302
Location: http://localhost:9999/callback?code=Bd-T2CSFQC35hpZhvuSikg&state=xyz123
POST /token -> HTTP 200
{'access_token': 'fvSS365Hvoun5aEw8GzJ6-b4EkgbSs1T', 'expires_in': 3600, 'token_type': 'Bearer'}
GET /userinfo -> HTTP 200
{'email': '[email protected]', 'name': 'Junior TSAFACK', 'sub': 'user-42'}

🎉 Le flux complet fonctionne, de bout en bout, avec un vrai serveur HTTP qui répond à de vraies requêtes.

Réutiliser un code d’autorisation déjà consommé

Section titled “Réutiliser un code d’autorisation déjà consommé”
first = requests.post(f"{BASE}/token", data={...}) # 1er echange, avec le meme "code"
second = requests.post(f"{BASE}/token", data={...}) # 2e echange, MEME code
print(second.status_code, second.json())
400 {'error': 'invalid_grant', 'error_description': 'code inconnu'}

Le code a été supprimé (del AUTH_CODES[code]) dès le premier échange réussi : un code d’autorisation est à usage unique, exactement comme prévu par la spécification.

401 {'error': 'invalid_client'}

PKCE : un code_verifier invalide (le code a été intercepté, mais pas le secret)

Section titled “PKCE : un code_verifier invalide (le code a été intercepté, mais pas le secret)”
400 {'error': 'invalid_grant', 'error_description': 'PKCE verifier invalide'}

Même en possession du code d’autorisation intercepté, un attaquant qui ne connaît pas le code_verifier d’origine ne peut pas terminer l’échange — exactement ce que PKCE est conçu pour empêcher (voir Module 3).

401 {'error': 'invalid_token'}

🤖 Bonus : le flux Client Credentials (machine-à-machine)

Section titled “🤖 Bonus : le flux Client Credentials (machine-à-machine)”

Sans navigateur, sans utilisateur — un service interne s’authentifie directement :

resp = requests.post(f"{BASE}/token", data={
"grant_type": "client_credentials",
"client_id": "service-facturation",
"client_secret": "s3cr3t-machine-a-machine",
})
print(resp.status_code, resp.json())
200 {'access_token': 'ZQDpQQWSpoCSEFN-ewjMZlBrVgiqyzJc', 'expires_in': 3600, 'token_type': 'Bearer'}

Ce token permet ensuite d’appeler directement une API interne, sans jamais impliquer d’utilisateur ni de navigateur — exactement le scénario du Module 2.

👉 Module 5 : Sécurité, pièges et OAuth vs authentification