feat(mcp): durcir le consentement OAuth (remember-cookie SameSite + CSRF) (#114)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Colin Maudry
2026-07-13 15:07:59 +02:00
parent bb2aee85a2
commit d2fa17761d
6 changed files with 61 additions and 4 deletions
+7 -3
View File
@@ -150,11 +150,15 @@ if _mcp_enabled:
oauth_routes.init_oauth(app.server)
# Exempter les routes OAuth du CSRF (POST externes JSON-RPC/form sans jeton).
# Exempter du CSRF les endpoints OAuth machine-à-machine (POST externes sans
# cookie) et les documents de découverte (GET). /oauth/authorize N'EST PAS
# exempté : endpoint de consentement authentifié par cookie de session, donc
# protégé par CSRF via un jeton dans le formulaire de consentement.
if _auth_csrf is not None:
_csrf_exempt_oauth = ("/oauth/token", "/oauth/register", "/oauth/revoke")
for _rule in app.server.url_map.iter_rules():
if _rule.rule.startswith("/oauth") or _rule.rule.startswith(
"/.well-known/oauth"
if _rule.rule in _csrf_exempt_oauth or _rule.rule.startswith(
"/.well-known/"
):
_vf = app.server.view_functions.get(_rule.endpoint)
if _vf is not None:
+3
View File
@@ -32,6 +32,9 @@ def init_auth(app: Flask) -> None:
app.config["SESSION_COOKIE_HTTPONLY"] = True
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
app.config["SESSION_COOKIE_SECURE"] = not DEVELOPMENT
app.config["REMEMBER_COOKIE_SAMESITE"] = "Lax"
app.config["REMEMBER_COOKIE_SECURE"] = not DEVELOPMENT
app.config["REMEMBER_COOKIE_HTTPONLY"] = True
app.config["PERMANENT_SESSION_LIFETIME"] = 60 * 60 * 24 * 30 # 30 jours
db.init_schema()
+2
View File
@@ -2,6 +2,7 @@ from urllib.parse import urlencode
from flask import redirect, request
from flask_login import current_user
from flask_wtf.csrf import generate_csrf
from src.mcp.oauth import consent
@@ -28,6 +29,7 @@ def authorize():
client.client_metadata.get("client_name", client.get_client_id()),
grant.request.redirect_uri or client.get_default_redirect_uri(),
scope,
csrf_token=generate_csrf(),
)
# POST
+4 -1
View File
@@ -21,7 +21,9 @@ def render_subscription_required() -> str:
)
def render_consent(client_name: str, redirect_uri: str, scope: str) -> str:
def render_consent(
client_name: str, redirect_uri: str, scope: str, csrf_token: str = ""
) -> str:
host = urlparse(redirect_uri).netloc or redirect_uri
return (
"<!doctype html><html lang=fr><head><meta charset=utf-8>"
@@ -31,6 +33,7 @@ def render_consent(client_name: str, redirect_uri: str, scope: str) -> str:
"colibre en votre nom via le connecteur MCP.</p>"
f"<p>Vous serez redirigé vers <strong>{escape(host)}</strong>.</p>"
'<form method="post">'
f'<input type="hidden" name="csrf_token" value="{escape(csrf_token)}">'
'<button name="confirm" value="yes" type="submit">Autoriser</button> '
'<button name="confirm" value="no" type="submit">Refuser</button>'
"</form></body></html>"