Aller au contenu

Proxy DCR

Le générateur DCR Proxy crée un proxy OAuth Dynamic Client Registration (DCR) devant un Amazon Cognito User Pool.

Les clients MCP (tels que Claude Code, Kiro CLI ou le MCP Inspector) s’attendent à s’authentifier auprès d’un serveur d’autorisation OAuth qui prend en charge Dynamic Client Registration et la découverte de métadonnées. Amazon Cognito ne prend pas en charge DCR nativement, et son secret App Client ne doit jamais être exposé à un client public. Ce proxy comble cette lacune : il maintient le flux Cognito Hosted UI intact, implémente DCR, injecte le secret App Client côté serveur lors de l’échange de jeton, et transmet le trafic MCP à votre serveur MCP en amont.

  1. Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
  2. Ouvrez la console Nx dans VSCode
  3. Cliquez sur Generate (UI) dans la section "Common Nx Commands"
  4. Recherchez @aws/nx-plugin - ts#dcr-proxy
  5. Remplissez les paramètres requis
    • Cliquez sur Generate
    ParamètreTypePar défautDescription
    name stringdcr-proxyLe nom de votre proxy DCR, utilisé pour son projet de gestionnaire TypeScript, le nom de la classe construct/module, et son répertoire sous common/constructs ou common/terraform
    directory stringpackagesLe répertoire dans lequel stocker le projet de gestionnaire du proxy DCR.
    subDirectory string-Le sous-répertoire dans lequel le projet de gestionnaire est placé. Par défaut, il s'agit du nom du projet.
    iac inherit | cdk | terraforminheritLe fournisseur IaC préféré (cdk ou terraform). Par défaut, cela est hérité de votre sélection initiale.
    preferInstallDependencies booleantrueIndique s'il faut privilégier l'installation des dépendances après l'exécution du générateur. Définir sur false pour différer l'installation lors du traitement par lots de plusieurs générateurs (une installation s'exécute toujours si nécessaire pour que les générateurs suivants puissent calculer le graphe de projet Nx) ; installer une fois à la fin.

    Le générateur crée un projet TypeScript autonome contenant les gestionnaires Lambda, et l’infrastructure pour les déployer en fonction de votre iac sélectionné.

    • Répertoire<dcr-proxy-name>
      • Répertoiresrc/
        • Répertoirehandlers/
          • authorization-server-metadata.ts Sert /.well-known/oauth-authorization-server et /.well-known/openid-configuration
          • protected-resource-metadata.ts Sert /.well-known/oauth-protected-resource
          • register.ts RFC 7591 Dynamic Client Registration
          • authorize.ts Redirige vers le Cognito Hosted UI
          • token.ts Injecte le secret App Client et échange le jeton
          • mcp-proxy.ts Proxie les requêtes /mcp vers le serveur MCP en amont

    Les gestionnaires sont regroupés indépendamment avec Rolldown, et les deux fournisseurs IaC référencent la sortie du bundle résultant.

    Ce générateur fournit de l’infrastructure as code basée sur votre iac choisi. Il créera un projet dans packages/common qui inclut les constructions CDK ou modules Terraform pertinents.

    Le projet commun d’infrastructure as code est structuré comme suit :

    • Répertoirepackages/common/constructs
      • Répertoiresrc
        • Répertoireapp/ Constructions pour l’infrastructure spécifique à un projet/générateur
        • Répertoirecore/ Constructions génériques réutilisées par celles dans app
        • index.ts Point d’entrée exportant les constructions depuis app
      • project.json Cibles de build et configuration du projet

    Pour déployer le proxy, les fichiers suivants sont générés :

    • Répertoirepackages/common/constructs/src
      • Répertoireapp
        • Répertoiredcr-proxies
          • Répertoire<dcr-proxy-name>
            • <dcr-proxy-name>.ts Construct CDK qui déploie le proxy

    L’infrastructure provisionne une API Gateway HTTP API avec les routes suivantes :

    RouteDescription
    GET /.well-known/oauth-protected-resourceMétadonnées de ressource protégée
    GET /.well-known/oauth-authorization-serverMétadonnées du serveur d’autorisation
    GET /.well-known/openid-configurationConfiguration OpenID (servie par le gestionnaire de métadonnées du serveur d’autorisation)
    POST /registerDynamic Client Registration
    GET /authorizeAutorisation (redirige vers le Cognito Hosted UI)
    POST /oauth/tokenÉchange de jeton (injecte le secret App Client)
    ANY /mcpProxy vers le serveur MCP en amont

    Seul le gestionnaire de jeton dispose d’un accès en lecture au secret Cognito App Client dans Secrets Manager.

    Le proxy ne crée pas vos ressources Cognito ni votre serveur MCP. Au lieu de cela, vous injectez les identifiants de ressources gérées ailleurs (qu’elles soient générées par ce plugin ou provisionnées séparément), gardant le proxy découplé de la manière dont ces ressources sont provisionnées.

    Vous fournissez :

    • L’id du Cognito User Pool et l’id de l’App Client
    • L’ARN d’un secret Secrets Manager contenant le secret App Client. Le gestionnaire de jeton le lit à l’exécution ; la valeur n’est jamais exposée au client.
    • L’URL de base du domaine Cognito Hosted UI
    • L’URL complète de votre serveur MCP en amont

    Instanciez le construct généré dans votre stack, en passant les propriétés requises :

    import { DcrProxy } from ':my-scope/common-constructs';
    new DcrProxy(this, 'DcrProxy', {
    userPoolId: userPool.userPoolId,
    userPoolClientId: userPoolClient.userPoolClientId,
    cognitoClientSecretArn: clientSecret.secretArn,
    cognitoHostedUiBase: userPoolDomain.baseUrl(),
    upstreamUrl: 'https://my-agentcore-runtime-url/mcp',
    });

    Le construct expose les points de terminaison du proxy (proxyUrl, mcpUrl, metadataUrl, tokenEndpoint, registrationEndpoint) en tant que propriétés en lecture seule.

    Pour fronter un serveur MCP généré avec le générateur ts#mcp-server (ou py#mcp-server) en utilisant --auth cognito, passez le même User Pool et App Client au serveur MCP et au proxy, et utilisez l’invocationUrl du construct du serveur MCP comme upstreamUrl du proxy.

    import {
    DcrProxy,
    MyProjectMcpServer,
    UserIdentity,
    } from ':my-scope/common-constructs';
    import { OAuthScope } from 'aws-cdk-lib/aws-cognito';
    import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
    const identity = new UserIdentity(this, 'Identity');
    // Un App Client confidentiel que le proxy utilise pour l'échange de jeton. Enregistrez les
    // URL de rappel que vos clients utilisent (voir ci-dessous).
    const proxyClient = identity.userPool.addClient('DcrProxyClient', {
    generateSecret: true,
    oAuth: {
    flows: { authorizationCodeGrant: true },
    scopes: [OAuthScope.OPENID, OAuthScope.EMAIL, OAuthScope.PROFILE],
    callbackUrls: [
    'http://localhost:41100/callback',
    // Callback utilisé par Claude Desktop
    'https://claude.ai/api/mcp/auth_callback',
    ],
    },
    });
    // Stockez le secret App Client dans Secrets Manager pour que le gestionnaire de jeton le lise
    const clientSecret = new secretsmanager.Secret(this, 'ClientSecret', {
    secretStringValue: proxyClient.userPoolClientSecret,
    });
    // Le serveur MCP, autorisant les JWT émis pour le même App Client
    const mcpServer = new MyProjectMcpServer(this, 'MyProjectMcpServer', {
    identity: {
    userPool: identity.userPool,
    userPoolClient: proxyClient,
    },
    });
    new DcrProxy(this, 'DcrProxy', {
    userPoolId: identity.userPool.userPoolId,
    userPoolClientId: proxyClient.userPoolClientId,
    cognitoClientSecretArn: clientSecret.secretArn,
    cognitoHostedUiBase: identity.userPoolDomain.baseUrl(),
    // Utilisez l'URL d'invocation du construct du serveur MCP plutôt que de la coder en dur
    upstreamUrl: mcpServer.invocationUrl,
    });

    Pour fronter une AgentCore Gateway générée avec le générateur agentcore-gateway en utilisant --auth cognito, passez le même User Pool et App Client à la gateway et au proxy, et utilisez le gatewayUrl du construct de la gateway comme upstreamUrl du proxy.

    import {
    DcrProxy,
    MyGateway,
    UserIdentity,
    } from ':my-scope/common-constructs';
    import { OAuthScope } from 'aws-cdk-lib/aws-cognito';
    import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
    const identity = new UserIdentity(this, 'Identity');
    // Un App Client confidentiel que le proxy utilise pour l'échange de jeton. Enregistrez les
    // URL de rappel que vos clients utilisent (voir ci-dessous).
    const proxyClient = identity.userPool.addClient('DcrProxyClient', {
    generateSecret: true,
    oAuth: {
    flows: { authorizationCodeGrant: true },
    scopes: [OAuthScope.OPENID, OAuthScope.EMAIL, OAuthScope.PROFILE],
    callbackUrls: [
    'http://localhost:41100/callback',
    // Callback utilisé par Claude Desktop
    'https://claude.ai/api/mcp/auth_callback',
    ],
    },
    });
    // Stockez le secret App Client dans Secrets Manager pour que le gestionnaire de jeton le lise
    const clientSecret = new secretsmanager.Secret(this, 'ClientSecret', {
    secretStringValue: proxyClient.userPoolClientSecret,
    });
    // La gateway, autorisant les JWT émis pour le même App Client
    const gateway = new MyGateway(this, 'MyGateway', {
    identity: {
    userPool: identity.userPool,
    userPoolClient: proxyClient,
    },
    });
    new DcrProxy(this, 'DcrProxy', {
    userPoolId: identity.userPool.userPoolId,
    userPoolClientId: proxyClient.userPoolClientId,
    cognitoClientSecretArn: clientSecret.secretArn,
    cognitoHostedUiBase: identity.userPoolDomain.baseUrl(),
    // Utilisez l'URL du construct de la gateway plutôt que de la coder en dur
    upstreamUrl: gateway.gateway.gatewayUrl,
    });

    Parce que le proxy implémente Dynamic Client Registration virtuellement — il n’y a pas d’App Client Cognito par client — chaque client MCP s’authentifie via le seul App Client que vous passez au proxy. Pendant le flux OAuth, le proxy transmet le redirect_uri du client au Cognito Hosted UI sans modification, donc Cognito effectue la vérification faisant autorité : le callback doit être enregistré en tant qu’URL de rappel sur cet App Client, sinon Cognito rejette la connexion.

    C’est un effet secondaire de la conception DCR virtuelle. Un client peut enregistrer n’importe quel redirect_uri avec le proxy, mais la connexion ne réussit que si cette URL exacte est l’une des URL de rappel de l’App Client. Cognito fait correspondre les URL de rappel exactement, y compris le port, donc les clients qui écoutent sur un port éphémère aléatoire ne peuvent pas être couverts par un caractère générique — vous devez épingler chaque client à une URL de rappel fixe et enregistrer cette URL exacte sur l’App Client.

    Ajoutez les URL de rappel que vos clients utilisent lorsque vous créez l’App Client :

    const userPoolClient = userPool.addClient('DcrProxyClient', {
    generateSecret: true,
    oAuth: {
    flows: { authorizationCodeGrant: true },
    callbackUrls: [
    // Clients locaux : épinglez à un port fixe et peu commun plutôt qu'à un port par défaut
    'http://localhost:41100/callback',
    // Callback utilisé par Claude Desktop
    'https://claude.ai/api/mcp/auth_callback',
    ],
    },
    });

    Le proxy permet aux clients MCP de s’authentifier auprès de votre Cognito User Pool sans aucune configuration spécifique au client au-delà de l’URL du proxy. Lorsqu’un client se connecte au point de terminaison /mcp, il découvre les métadonnées OAuth (via /.well-known/oauth-protected-resource et /.well-known/oauth-authorization-server), s’enregistre dynamiquement et guide l’utilisateur à travers la connexion Cognito Hosted UI. Le proxy injecte le secret App Client lors de l’échange de jeton, donc le client n’en a jamais besoin.

    Pour connecter un client, pointez-le vers le mcpUrl du proxy (c’est-à-dire <proxyUrl>/mcp). Les exemples ci-dessous supposent que votre proxy est déployé sur https://my-proxy.example.com.

    Ajoutez le serveur proxié avec la commande claude mcp add, en utilisant le transport HTTP. Par défaut, Claude Code écoute sur un port de rappel aléatoire ; passez --callback-port pour l’épingler au port enregistré sur votre App Client (41100 dans les exemples ci-dessus). Claude Code utilise toujours le chemin /callback, donc l’URI de redirection résultant est http://localhost:41100/callback :

    Fenêtre de terminal
    claude mcp add --transport http --callback-port 41100 my-proxied-server https://my-proxy.example.com/mcp

    Lorsque vous invoquez pour la première fois un outil du serveur, Claude Code ouvre le Cognito Hosted UI pour s’authentifier avant que la requête ne soit proxiée en amont.

    Ajoutez le serveur à votre configuration MCP Kiro CLI, en utilisant le transport HTTP. Sans oauth.redirectUri explicite, Kiro choisit un port de rappel aléatoire ; définissez-le sur l’URL enregistrée sur votre App Client afin que le port et le chemin correspondent exactement :

    {
    "mcpServers": {
    "my-proxied-server": {
    "type": "http",
    "url": "https://my-proxy.example.com/mcp",
    "oauth": {
    "redirectUri": "http://localhost:41100/callback"
    }
    }
    }
    }

    Kiro CLI déclenche la connexion Cognito Hosted UI lors de la première utilisation et gère les jetons résultants pour les requêtes ultérieures.

    Si vous avez déjà un User Pool provenant du générateur ts#website#auth (le construct UserIdentity), vous pouvez fronter un serveur MCP pour les mêmes utilisateurs qui se connectent à votre site web. Réutilisez son userPool, mais ajoutez un App Client séparé pour le proxy : le client du site web est un client public sans secret, tandis que le proxy DCR nécessite un client confidentiel (generateSecret: true) dont le secret est injecté par le gestionnaire de jeton lors de l’échange de jeton.

    UserIdentity configure le domaine User Pool avec Managed Login (version 2). Managed Login nécessite un style de branding par App Client, vous devez donc en créer un pour le nouveau client proxy — sinon sa page de connexion hébergée renvoie 403.

    import {
    DcrProxy,
    MyProjectMcpServer,
    UserIdentity,
    } from ':my-scope/common-constructs';
    import { OAuthScope, CfnManagedLoginBranding } from 'aws-cdk-lib/aws-cognito';
    import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
    // Le user pool créé par ts#website#auth pour les utilisateurs de votre site web
    const identity = new UserIdentity(this, 'Identity');
    // Un App Client confidentiel sur le MÊME user pool pour le proxy DCR
    const proxyClient = identity.userPool.addClient('DcrProxyClient', {
    generateSecret: true,
    oAuth: {
    flows: { authorizationCodeGrant: true },
    scopes: [OAuthScope.OPENID, OAuthScope.EMAIL, OAuthScope.PROFILE],
    callbackUrls: ['http://localhost:41100/callback'],
    },
    });
    // Managed Login nécessite un style de branding pour le nouveau client
    new CfnManagedLoginBranding(this, 'DcrProxyClientBranding', {
    userPoolId: identity.userPool.userPoolId,
    clientId: proxyClient.userPoolClientId,
    useCognitoProvidedValues: true,
    });
    const clientSecret = new secretsmanager.Secret(this, 'ClientSecret', {
    secretStringValue: proxyClient.userPoolClientSecret,
    });
    const mcpServer = new MyProjectMcpServer(this, 'MyProjectMcpServer', {
    identity: {
    userPool: identity.userPool,
    userPoolClient: proxyClient,
    },
    });
    new DcrProxy(this, 'DcrProxy', {
    userPoolId: identity.userPool.userPoolId,
    userPoolClientId: proxyClient.userPoolClientId,
    cognitoClientSecretArn: clientSecret.secretArn,
    // Le construct UserIdentity crée toujours un domaine
    cognitoHostedUiBase: identity.userPoolDomain.baseUrl(),
    upstreamUrl: mcpServer.invocationUrl,
    });