feat(mcp): flux OAuth authorize+token avec gate abonnement (#114)
Remplace le stub 501 de src/mcp/oauth/authorize.py par le flux réel authlib (GET consentement, POST émission de code, échange de token), avec gate d'abonnement avant tout affichage du consentement. Deux ajustements de compatibilité authlib 1.7.2 dans server.py, découverts en exécutant le flux bout-en-bout pour la première fois : AUTHLIB_INSECURE_TRANSPORT en mode DEVELOPMENT (le client de test Flask ne sert pas en HTTPS) et OAUTH2_REFRESH_TOKEN_GENERATOR (off par défaut côté Flask, requis pour émettre les refresh_token du scope offline_access). Détails dans .superpowers/sdd/task-9-report.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,42 @@
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from flask import redirect, request
|
||||
from flask_login import current_user
|
||||
|
||||
from src.mcp.oauth import consent
|
||||
|
||||
|
||||
def _login_redirect():
|
||||
target = f"/oauth/authorize?{urlencode(request.args)}"
|
||||
return redirect(f"/connexion?next={target}")
|
||||
|
||||
|
||||
def authorize():
|
||||
return "", 501
|
||||
from src.mcp.oauth.routes import _server
|
||||
|
||||
if not current_user.is_authenticated:
|
||||
return _login_redirect()
|
||||
|
||||
if not consent.subscription_ok(int(current_user.id)):
|
||||
return consent.render_subscription_required(), 403
|
||||
|
||||
if request.method == "GET":
|
||||
grant = _server.get_consent_grant(end_user=current_user)
|
||||
client = grant.client
|
||||
scope = grant.request.scope or "mcp"
|
||||
return consent.render_consent(
|
||||
client.client_metadata.get("client_name", client.get_client_id()),
|
||||
grant.request.redirect_uri or client.get_default_redirect_uri(),
|
||||
scope,
|
||||
)
|
||||
|
||||
# POST
|
||||
if request.form.get("confirm") != "yes":
|
||||
return _server.create_authorization_response(grant_user=None)
|
||||
return _server.create_authorization_response(grant_user=int(current_user.id))
|
||||
|
||||
|
||||
def token():
|
||||
return "", 501
|
||||
from src.mcp.oauth.routes import _server
|
||||
|
||||
return _server.create_token_response()
|
||||
|
||||
@@ -9,10 +9,18 @@ from authlib.oauth2.rfc7591 import ClientRegistrationEndpoint
|
||||
from authlib.oauth2.rfc7636 import CodeChallenge
|
||||
|
||||
from src.mcp.oauth import metadata, store
|
||||
from src.utils import DEVELOPMENT
|
||||
|
||||
ACCESS_TTL = 3600 # 1 h
|
||||
REFRESH_TTL = 5184000 # 60 j
|
||||
|
||||
if DEVELOPMENT:
|
||||
# authlib refuse tout URI non-https (InsecureTransportError), y compris
|
||||
# http://colibre.fr utilisé par le client de test Flask (pas de TLS local).
|
||||
# Comportement identique à SESSION_COOKIE_SECURE = not DEVELOPMENT dans
|
||||
# src/auth/setup.py : ne s'applique jamais en production (DEVELOPMENT=false).
|
||||
os.environ.setdefault("AUTHLIB_INSECURE_TRANSPORT", "1")
|
||||
|
||||
|
||||
def _db() -> str:
|
||||
return os.environ["USERS_DB_PATH"]
|
||||
@@ -224,6 +232,10 @@ class _RevocationEndpoint(RevocationEndpoint):
|
||||
|
||||
|
||||
def create_authorization_server(app) -> AuthorizationServer:
|
||||
# authlib n'émet un refresh_token que si ce flag est activé (défaut: False) ;
|
||||
# requis puisque RefreshTokenGrant est enregistré ci-dessous et que le scope
|
||||
# "offline_access" (DCR) suppose l'émission d'un refresh_token.
|
||||
app.config.setdefault("OAUTH2_REFRESH_TOKEN_GENERATOR", True)
|
||||
server = AuthorizationServer(app, query_client=query_client, save_token=save_token)
|
||||
server.register_grant(AuthorizationCodeGrant, [CodeChallenge(required=True)])
|
||||
server.register_grant(RefreshTokenGrant)
|
||||
|
||||
Reference in New Issue
Block a user