Aller au contenu principal

OAuth 2.0 pour Serveurs MCP

Pourquoi OAuth dans MCP ?

Quand un serveur MCP accède à des données personnelles — emails, calendrier, dépôts GitHub, tickets Linear — il doit prouver qu’il agit au nom de l’utilisateur. Un simple token statique ne suffit pas : il faut un mécanisme de consentement explicite, de droits limités (scopes), et de renouvellement sécurisé. C’est exactement ce que propose OAuth 2.0.

Les trois niveaux d’authentification MCP (rappel)

  1. Aucune auth : URL publique, pas de données sensibles
  2. Token statique : clé API générée manuellement (GitHub Personal Access Token)
  3. OAuth 2.0 : flux de consentement complet avec scopes et refresh tokens

Le flux OAuth 2.0 dans MCP

Quand un utilisateur connecte un serveur MCP avec OAuth dans Le Chat :

1. L utilisateur clique "Connecter" dans Le Chat

2. Le Chat redirige vers la page d autorisation du service
   (ex: GitHub, Google, Linear)

3. L utilisateur accorde les permissions demandées (scopes)

4. Le service renvoie un code d autorisation au callback

5. Le client échange le code contre un access token + refresh token

6. L access token est utilisé pour chaque appel MCP

7. Quand l access token expire, le refresh token en obtient un nouveau

Les tokens en jeu

  • Access token : utilisé à chaque requête, durée de vie courte (1h typiquement)
  • Refresh token : permet d’obtenir un nouvel access token sans reconsentement
  • Authorization code : temporaire, échangé une seule fois contre les tokens

Configurer OAuth côté client Python

Le SDK Mistral fournit un helper build_oauth_params pour simplifier la configuration OAuth :

from mistralai.extra.mcp.auth import build_oauth_params

# Construire les paramètres OAuth
oauth_params = build_oauth_params(
    client_id="votre_client_id",
    client_secret="votre_client_secret",
    authorize_url="https://github.com/login/oauth/authorize",
    token_url="https://github.com/login/oauth/access_token",
    scopes=["repo", "issues"],
    redirect_uri="http://localhost:3000/callback",
)

Le helper gère automatiquement :

  • Le lancement d’un serveur callback local pour recevoir le code d’autorisation
  • L’échange du code contre les tokens
  • Le stockage et le renouvellement des tokens

Tokens et scopes : le principe du moindre privilège

Les scopes définissent ce que le serveur MCP peut faire au nom de l’utilisateur. Demandez toujours le minimum nécessaire :

# Mauvais : trop de permissions
scopes=["repo", "admin:org", "delete_repo", "user"]

# Bon : juste ce qu il faut pour créer des issues
scopes=["repo:issues", "read:user"]

Chaque service a ses propres scopes. Consultez la documentation OAuth du service cible pour choisir les bons.

Sécurité des tokens

Stockage

Les tokens doivent être stockés de manière sécurisée :

import os
import json
from pathlib import Path

TOKEN_FILE = Path.home() / ".mcp" / "tokens.json"

def save_tokens(service: str, access_token: str, refresh_token: str):
    """Sauvegarde les tokens de manière sécurisée."""
    TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
    tokens = {}
    if TOKEN_FILE.exists():
        tokens = json.loads(TOKEN_FILE.read_text())
    tokens[service] = {
        "access_token": access_token,
        "refresh_token": refresh_token,
    }
    TOKEN_FILE.write_text(json.dumps(tokens))
    # Restreindre les permissions du fichier
    os.chmod(TOKEN_FILE, 0o600)

Renouvellement automatique

Quand l’access token expire, le refresh token permet d’en obtenir un nouveau sans intervention de l’utilisateur :

import httpx

async def refresh_access_token(
    token_url: str,
    client_id: str,
    client_secret: str,
    refresh_token: str,
) -> dict:
    """Renouvelle l access token via le refresh token."""
    async with httpx.AsyncClient() as http:
        response = await http.post(
            token_url,
            data={
                "grant_type": "refresh_token",
                "refresh_token": refresh_token,
                "client_id": client_id,
                "client_secret": client_secret,
            },
        )
        response.raise_for_status()
        return response.json()

Serveurs MCP avec authentification par token

Pour les cas plus simples (API key), pas besoin d’OAuth complet. Un header HTTP suffit :

from mistralai.extra.mcp.sse import MCPClientSSE, SSEServerParams

# Serveur avec token simple
mcp_client = MCPClientSSE(
    sse_params=SSEServerParams(
        url="https://mon-serveur.com/mcp",
        headers={"Authorization": "Bearer mon_api_token_secret"},
        timeout=60,
    )
)

Côté serveur, validez le token dans un middleware :

from fastapi import FastAPI, Request, HTTPException

api = FastAPI()

@api.middleware("http")
async def verify_token(request: Request, call_next):
    if request.url.path.startswith("/mcp"):
        token = request.headers.get("Authorization", "")
        if token != "Bearer mon_api_token_secret":
            raise HTTPException(status_code=401, detail="Non autorisé")
    return await call_next(request)

OAuth dans Le Chat de Mistral

Dans Le Chat, les connecteurs featured (GitHub, Gmail, Linear) gèrent OAuth automatiquement. Pour vos connecteurs custom, trois options sont proposées à la configuration :

  1. No authentication — URL publique
  2. Token — Vous collez un token manuellement
  3. OAuth 2.0 — Flux de consentement complet

Le token est stocké par utilisateur, pas par organisation. Si vous êtes dans une équipe Mistral, chaque membre a ses propres credentials.

Points clés à retenir

  • OAuth 2.0 permet un consentement explicite avec des droits limités (scopes)
  • Le flux : autorisation → code → tokens (access + refresh)
  • Appliquez le principe du moindre privilège sur les scopes
  • Les tokens doivent être stockés de manière sécurisée et renouvelés automatiquement
  • Pour les cas simples, un token statique dans les headers HTTP suffit
  • Dans Le Chat, les tokens sont stockés par utilisateur