Tutoriel
Comment un client MCP fige l’issuer OAuth : ce qui reste après Python SDK 1.30 et 2.2
OAuth côté serveur décide seulement qui peut appeler un outil. L’avis du 28 septembre pose une question au client : acceptez-vous le service de connexion que la découverte vient de nommer ?
Le 28 septembre 2026, les mainteneurs du SDK Python MCP officiel ont confirmé qu’un client HTTP vulnérable pouvait envoyer le client secret, l’authorization code et la preuve PKCE destinés au vrai service de connexion vers un point token contrôlé par l’autre partie. Les versions corrigées sont 1.30.0 sur la ligne 1.x et 2.2.0 sur la ligne 2.x. Elles sont sorties le 7 septembre, et les notes de version parlaient d’un changement de comportement. L’avis lui-même date du 28 septembre, le jour où Cycode a publié sa recherche ; la presse sécurité l’a repris le lendemain. Le récit de départ est le compte-rendu de The Hacker News. La forme correcte du client aujourd’hui est OAuth clients. Comment un serveur exige un token reste l’article MCP OAuth 2.1. Quand le serveur distant n’est pas entièrement le vôtre, la forme de déploiement est dans MCP distant en production.
L’avis corrige le client, pas un serveur que vous avez déjà protégé
Ce qui est touché, c’est un processus qui utilise le SDK Python officiel comme client HTTP, avec OAuth activé. Il se connecte à un serveur MCP auquel il ne fait pas entièrement confiance, tout en détenant les secrets d’un vrai service de connexion. Les mainteneurs disent que certains chemins de découverte n’avaient pas fixé le service attendu avant d’accepter celui que le serveur nommait. Les versions corrigées décident d’abord de l’issuer attendu, puis vont chercher les métadonnées du serveur d’autorisation. Le champ issuer du document doit être cette même partie, sinon le client refuse. Les secrets stockés portent le service de connexion : un enregistrement marqué pour A ne sert pas à échanger un token chez B.
Ce n’est pas la même couche que « ajouter OAuth 2.1 à un serveur MCP ». Le serveur décide qui peut appeler un outil et si le token a été émis pour cette ressource. Le client décide si le serveur d’autorisation nommé par la découverte est celui qui a émis le client secret. Il faut les deux. Un serveur protégé n’empêche pas un client de suivre la mauvaise partie pendant la découverte. Un client corrigé ne couvre pas un serveur qui saute encore le contrôle d’audience.
Le changement arrivé sur la ligne principale est la PR 3398. Les métadonnées du serveur d’autorisation doivent nommer, dans issuer, le serveur auprès duquel on les a lues. La règle est RFC 8414, section 3.3. En cas d’écart, on s’arrête. On n’ajoute pas d’interrupteur client qui ignore issuer.
| Ce que vous faites tourner | Cet avis | Encore à faire après la montée |
|---|---|---|
| Client HTTP, OAuthClientProvider | 1.9.1–1.29.1, ou 2.0.0–2.1.1 | Passer à 1.30.0 ou 2.2.0 et supprimer l’ancien client_info |
| Client credentials ou JWT à clé privée | Les mêmes plages de versions | Passer aussi issuer |
| Serveur SDK, stdio, ou token à vous | Hors de cet avis | Monter quand même la dépendance sur une version corrigée |
Séparer d’abord qui est dans le périmètre
En 1.x, la plage va de 1.9.1 à 1.29.1. En 2.x, de 2.0.0 à 2.1.1. Les classes sont OAuthClientProvider, ClientCredentialsOAuthProvider, PrivateKeyJWTOAuthProvider, et le RFC7523OAuthClientProvider déprécié, qui n’a pas d’argument issuer. Cycode a classé le problème comme élevé. Le stdio local, les clients qui posent leur propre token, et les déploiements qui n’utilisent le SDK que pour écrire un serveur sont hors de cet avis.
Un périmètre étroit ne veut pas dire que le dépôt peut garder l’ancien pin. Un CLI, un job de nuit et un client de bureau partagent souvent une seule dépendance mcp dans le même lockfile. Le schéma dit que seul le serveur utilise le SDK, et un client HTTP en 1.29 est quand même installé. On monte selon la version du lockfile, pas selon le rôle sur la diapositive.
Sur les anciennes versions, l’avis ne donne qu’une règle temporaire : un client avec OAuth ne se connecte qu’à des serveurs de confiance. S’il s’est déjà connecté à un serveur qui ne l’est pas, faites tourner le client secret chez le service de connexion et révoquez les tokens émis. Cette étape concerne des secrets qui ont peut-être déjà quitté votre environnement. Ce n’est pas une invitation à rejouer la découverte.
Après 1.30 ou 2.2, le machine à machine a encore besoin d’issuer
Le flux navigateur, OAuthClientProvider, décide du service de connexion attendu avant d’aller chercher les métadonnées. Les deux flux sans navigateur ne le font pas. ClientCredentialsOAuthProvider et PrivateKeyJWTOAuthProvider ont besoin d’issuer, égal à la valeur issuer du document /.well-known/oauth-authorization-server de ce serveur d’autorisation. La découverte tourne encore, mais les demandes de token ne sont bâties qu’avec les métadonnées de cette partie. Si le serveur MCP pointe ailleurs, le flux s’arrête sur OAuthFlowError.
Sans issuer, ces deux providers suivent encore ce que la découverte renvoie, même sur la version corrigée. L’avis est net : sans l’argument, la montée n’atteint pas le machine à machine. Sur 1.30.0, le rappel est un DeprecationWarning ordinaire, caché par défaut par Python, donc souvent absent des logs de CI. En 3.0 l’argument devient obligatoire. Posez-le maintenant. N’attendez pas que la version majeure fasse échouer le job.
RFC7523OAuthClientProvider n’a pas d’argument issuer. Y rester veut dire que la montée ne peut pas figer le service de connexion. Passez à ClientCredentialsOAuthProvider ou PrivateKeyJWTOAuthProvider, puis passez issuer. Lisez le secret depuis l’environnement ou un gestionnaire de secrets. Ne le commitez pas.
Dans des métadonnées de confiance, issuer doit égaler l’URL lue
Allez chercher le document sur un serveur d’autorisation auquel vous faites déjà confiance, par exemple https://auth.example.com/.well-known/oauth-authorization-server. Le issuer dedans doit être ce serveur. La comparaison est une égalité de chaînes, sans normalisation. Un slash final, une casse d’hôte différente ou un préfixe www est une autre valeur. Passez au client la chaîne exacte du document. Ne tapez pas une URL qui ne fait que ressembler.
Ce JSON est le document de métadonnées d’un serveur d’autorisation de confiance. issuer correspond à l’origine lue, et le point token est sur le même hôte.
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"],
"code_challenge_methods_supported": ["S256"],
"authorization_response_iss_parameter_supported": true
}
Les métadonnées de ressource protégée disent seulement au client quel serveur d’autorisation protège ce serveur MCP. Elles ne remplacent pas l’identité du service de connexion. Le client va encore lire les métadonnées de ce serveur et vérifier issuer. Si les métadonnées de ressource nomment A et que le document du serveur d’autorisation se nomme B, on s’arrête. On ne fige pas B juste pour faire passer l’intégration.
La révision du 28 juillet 2026 rabaisse l’enregistrement dynamique au profit d’un Client ID Metadata Document. Vous publiez un JSON client à une URL HTTPS stable, et cette URL est le client_id. Le SDK la prend dans client_metadata_url. Quand le serveur d’autorisation annonce le support, le client ne demande plus à /register une paire id et secret neuve. L’URL doit être en HTTPS avec un chemin qui n’est pas la racine, contrôlé dès la construction. Le client_info déjà stocké gagne encore sur ce document : un vieil enregistrement laissé en place, et le nouvel issuer ne prend pas effet.
Effacer l’ancien client_info, et faire tourner les secrets à part
La version corrigée étiquette les enregistrements stockés avec le service de connexion. Le client_info écrit avant la montée n’a pas cette étiquette. L’avis demande de supprimer ces enregistrements pour que la prochaine inscription soit créée selon la nouvelle règle et liée à la bonne partie. Monter le paquet et garder l’ancien fichier, c’est laisser le processus ressortir un enregistrement non lié.
Effacer l’enregistrement et faire tourner le secret sont deux travaux. Vider client_info permet au nouveau code de s’enregistrer à nouveau. Faire tourner le client secret et révoquer les tokens chez le service de connexion compte parce que le secret a peut-être déjà été présenté à un serveur qui n’est pas de confiance. Sans cet historique, on ne fait pas tourner chaque client interne. Avec cet historique, on le fait chez le service de connexion. On ne colle pas le secret dans un journal de discussion.
Rendez state et iss du callback au provider tels qu’ils sont arrivés. Le provider compare state à la valeur qu’il a générée et iss à l’issuer découvert. Ces deux champs sont les contrôles contre le mélange. Ce ne sont pas des champs de debug. Un parseur de callback qui jette iss retire un contrôle que la version corrigée vient d’ajouter.
Mettre l’issuer figé dans le dépôt et le relire comme un contrat
Chaque point MCP a un enregistrement : l’URL du serveur, et l’issuer que vous autorisez. Copiez la valeur telle quelle depuis les métadonnées du serveur d’autorisation. On relit le JSON formaté, pas un nom d’hôte pris dans des notes. Entre deux mises en ligne, le diff montre qui a déplacé issuer vers un autre hôte.
Gardez cette carte dans le dépôt. Ne gardez pas client_secret ici. Le secret reste dans le gestionnaire de secrets. Ce fichier ne fige que le service de connexion.
{
"clients": [
{
"mcp_server": "https://orders.example.com/mcp",
"issuer": "https://auth.example.com"
}
]
}
Le constructeur machine à machine et cette liste doivent utiliser la même chaîne. La liste dit https://auth.example.com et le code ajoute un slash : le client corrigé traite ça comme une autre partie et s’arrête. C’est l’échec que vous voulez. Le job passe au rouge sur OAuthFlowError, au lieu de changer de point token en silence.
Avant de committer, utilisez le formateur JSON pour étaler les métadonnées et la liste d’issuer, la validation JSON Schema pour confirmer que issuer et token_endpoint sont des chaînes, et le diff JSON pour voir qui a changé le service de connexion.
Questions fréquentes
Si nous ne faisons tourner qu’un serveur stdio local, est-ce que ça nous concerne ?
Un client stdio est hors de cet avis. Si le même environnement a aussi un client HTTP sur ce paquet, montez selon la version de ce client. Un lockfile qui contient 1.9.1 à 1.29.1, ou 2.0.0 à 2.1.1, passe à la version corrigée correspondante. On ne saute pas l’étape parce que le schéma dit stdio.
Nous sommes passés à 1.30.0 et le journal ne montre aucun avertissement. C’est fini ?
Non. Quand ClientCredentialsOAuthProvider ou PrivateKeyJWTOAuthProvider est construit sans issuer, le rappel est un DeprecationWarning, caché par défaut. Activez les avertissements de dépréciation, ou passez issuer au constructeur. Un journal silencieux ne veut pas dire que l’argument est posé.
Enregistrement dynamique ou Client ID Metadata Document ?
Quand le serveur d’autorisation annonce client_id_metadata_document_supported, publiez un JSON client en HTTPS et utilisez cette URL comme client_id. Vous cessez de demander un secret à /register à chaque rencontre. S’il ne l’annonce pas, le SDK retombe sur l’enregistrement dynamique. Dans les deux cas, le client_info stocké gagne : supprimez l’ancien enregistrement après la montée.
Comment cet article se place-t-il à côté de celui sur OAuth 2.1 côté serveur ?
L’article serveur décrit comment le serveur de ressource exige un access token. Celui-ci décrit comment le client ne suit pas le mauvais service de connexion. Quand le serveur distant n’est pas derrière votre propre passerelle, lisez les deux : le serveur vérifie le token, le client fige issuer dans le constructeur et dans la liste du dépôt.
À retenir
L’avis du 28 septembre retire « croire qui la découverte nomme » du comportement par défaut. Passez les clients HTTP à 1.30.0 ou 2.2.0. Pour le machine à machine, passez un issuer identique au texte des métadonnées. Remplacez le provider RFC7523 déprécié.
Puis supprimez l’ancien client_info. Si le client a parlé à un serveur qui n’est pas de confiance, faites tourner le secret et révoquez les tokens chez le service de connexion. Ne gardez dans le dépôt que l’URL du serveur et l’issuer, et surveillez cette chaîne avec un formateur, un contrôle de schéma et un diff.