5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude CodeからAgentCore GatewayへのSSO接続を作り直すついでにOAuthのクライアント登録を学ぶ

5
Posted at

はじめに

こんにちは、ふくちと申します。

以前、Claude Codeから社内向けのMCPサーバー(AgentCore Gateway)へ、会社のSSOログインで接続する構成を書きました。

この構成のキモはLambdaのDCR Proxyです。
CognitoがDCR(Dynamic Client Registration、クライアント情報を動的に登録する仕組み)に対応していないため、登録処理を代行させるために作ったものです。

ただこれを使うとCognitoアプリケーションクライアントが無限に増殖していくので、どこかで実装を修正しないといけないなーと思っていました。

そこで色々調べていると、Claude Code v2.1.30以降の事前登録クライアント対応で--client-id--callback-portを指定できるようになっていたようです。だいぶ前ですが私は気づいていませんでした(執筆時点でv2.1.232とか)

これがあれば、DCR構成を取らなくても良くなりそうなので、やってみました。

以前と同じく認証まわりに詳しくない人でも追えるように努めますが、用語が不安な方は以前の記事の用語解説を先に眺めてもらえると読みやすいと思います。

DCR版の問題点

以前の構成を簡単に解説しておきます。
Claude CodeはAgentCore Gatewayへ/mcpで接続し、AgentCore GatewayはJWTを検証して正規ユーザーだけがアクセスできるようにします。

トークンの発行役がAmazon Cognitoで、実際のログインは会社のEntra IDが担います。
そしてCognitoが提供していないDCRの登録エンドポイントをLambdaで実装していた形です。

ざっくり流れとしてはこんな感じ。

image.png

DCRを使う場合、MCP接続とクライアント登録は基本的にセットで行われます。

ローカル環境にクライアント登録情報がない状態でMCPサーバーと接続(/mcp)しようとすると、まずは登録エンドポイント(/register)にアクセスしてCognitoアプリケーションクライアントが1つ作られます。

そのクライアントを使うことで、Claude CodeはMCPサーバーと接続できるようになります。
なので初回のMCP接続時や再認証時などには必ずクライアント登録が行われたうえでMCPサーバーとの接続がなされる、というわけです。

ただここで1つ問題があります。認証し直すたびにアプリケーションクライアントが1つずつ増えていくのです。
増えるだけならまぁそれほど問題ではないのですが、Cognitoのアプリケーションクライアント数の上限はデフォルト1,000件までになっています(執筆時点)。

上限に達すると、以降新しいアプリケーションクライアントの作成が失敗するため、新しく接続しようとする正規ユーザーの登録が失敗することになります。
つまりこの構成のまま利用者が増加していくと、誰も新規接続できない状態に行き着く可能性があります。既存ユーザーの再認証も通らなくなりますね。

これはシンプルに時間の問題として起きる可能性がある問題ですし、悪意があれば登録エンドポイントを叩き続けるだけで意図的に問題を起こすことができます。ログインさせないことを狙う攻撃が成立してしまうわけです。

そうなると今度はアプリケーションクライアントの管理を自前で行わなければいけなくなったり、DCR Proxyへのリクエストで厳密に防がないといけなかったりと、運用の負担が増えます。

攻撃を防ぎたいならDCRの登録エンドポイントに認証でもつけてカバーすれば良いのでは、と思う方がいるかもしれませんが、以前の記事で記載した通りそれは難しいです。

なぜならDCRの仕様上、登録エンドポイントはまだクライアントとして登録されていない人からのリクエストを受ける必要があるので、必然的に認証なしで公開することになるんですね。あちらを立てればこちらが立たずな感じです。

運用の手間は無い方が良いし、構成はシンプルな方が良いですからね。他に良い方法は無いものかと探していました。

Claude Codeの事前登録クライアント対応

すると普通にありました。アプリケーションクライアントを事前に1つ作成しておいて、このclient_idで認証してほしいというのをMCP設定で指定すれば良いみたいです。

Claude CodeでMCPを追加するコマンドclaude mcp addにおいて、--client-id--callback-portが(だいぶ前に)追加されていたので、管理者がアプリケーションクライアントを1つ作り、そのIDを利用者に配ればそれでOKでした。

そもそもDCRを使っていた理由は、Claude Code側にこのclient_idで認証してと指定する方法がないと思い込んでいたからです。つまり私の調査不足!

だいぶ遠回りでしたが、OAuth本来の形というかシンプルな構成にこれで戻ってきました。
とはいえDCRも面白かったんですけどね、ちょっとCognitoとは相性が良くなかったのかもしれません。

移行後の構成はこんな感じです。

image.png

あらかじめアプリケーションクライアントを作成しておき、そのクライアントIDからのリクエストのみを受け付けるようにAgentCore Gatewayを設定しています。

Cognitoへのログインに関しては、会社SSOでできると体験が良いと思います。マストではないですが。

ここまでを踏まえて、前の構成と新しい構成のポイントを比較するとこんな感じです。

観点 移行前(DCR Proxy) 移行後(事前登録)
アプリクライアント数 接続ごとに増加 1つのみ
自作コンポーネント DCR Proxy Lambda、ステータス管理DB、アプリクライアント管理ツールなど なし
認証なしで公開するもの Lambda関数URL なし
Gatewayが参照するDiscovery URL 自作Proxyのwell-knownエンドポイント Cognito
クライアントの固定 不可(動的に増えるため) allowedClientsで固定
利用者の接続手順 Gateway URLのみ client_idとcallback portの指定が必要

構成こそ変わりましたが、MCPを登録する際は以前と同じく1行でOKなので、利用者の体験的には特に変わりないはずです。

用語解説

本記事で重要な3つの用語を解説します。

事前登録クライアント

OAuthのクライアント登録には大きく2種類あります。

  • 管理者があらかじめ登録しておく
  • 接続時にプログラムが自分で登録する動的登録(DCR)

世の中の大半のOAuth連携は事前登録で動いています。
一方、DCRは誰が接続してくるか事前に分からない場面のための仕組みです。以前の記事でも書いた通り、DCRはMCPが登場してきて再度注目を浴びたものです。

前回はDCRを選択しましたが、今回はスタンダードな事前登録を採用しています。

OIDCディスカバリー

認可サーバーがトークンの発行や検証に必要な情報のある場所を外部に公開するための設定ファイルです。
/.well-known/openid-configurationというURLで公開されます。

ただ、Cognito公式のこの設定ファイルには、DCR用の登録エンドポイントが含まれていません。なので以前は公式の内容に登録エンドポイントを書き足した改造版をDCR Proxyから配信し、Claude Codeはそれを読んで /register の場所を知る、という実装にしていました。

今回はCognito公式のものをそのまま使うことができるので、シンプルになりました。

ちなみにこのURL自体はCognitoユーザープールの概要ページに"OpenID Connect設定URL"として記載されています。

また、AgentCore Gateway/RuntimeではこのディスカバリーURLを設定する項目があるので、ここに設定してあげるようなイメージで実装していきます。後述のクライアントIDもここで指定可能。

image.png

AgentCore GatewayのallowedClients

AgentCore Gatewayのインバウンド認証における設定の1つで、この許可リストにあるclient_id宛てに発行されたトークン以外は拒否するという指定です。

以前のDCR構成ではclient_idが動的に増え続けるため、これを設定しようがありませんでした。

しかし本構成ではクライアントを1つに固定できたので設定しています。これでよりセキュアに運用することができるようになりました。

認証のフロー

以前のブログにて、認証フローは5つのフェーズ(①DCR ②SSOログイン ③トークン取得 ④トークン検証 ⑤API呼び出し)があると説明しました。

そこから新しい構成になったことで、①と④だけ少し変わります。

フェーズ1は処理そのものがDCRからクライアント事前登録に置き換わります。前述の通りですね。

MCP接続のたびにClaude Codeがクライアントを自動登録するのではなく、開発者側が事前にCognitoアプリケーションクライアントを登録しておく。
利用者はMCPを設定する際に、そのclient_idを指定して接続する。全体としてこんな流れになります。

動的な登録がなくなったので、DCR Proxyとそれを管理する仕組みもまるっと不要になりました。
AgentCore Gatewayが参照するディスカバリーURLもDCR ProxyからCognito公式のものに変更しています。

フェーズ2(SSOログイン)とフェーズ3(トークン取得)の大枠は変わりません。ブラウザでEntra IDにログインし、Cognitoが認可コードを発行し、Claude CodeがPKCEの検証を通してトークンへ交換します。
一応差分は1点だけあり、認可コードの受け取り先がhttp://localhost:53682/callbackに固定されます(理由は後述)。

フェーズ4(トークン検証)は違いこそ少しですが、セキュリティが強化されます。
AgentCore Gatewayでの署名・有効期限・グループの検証に加えて、トークンのclient_idが許可リスト(allowedClients)と一致するかも検証するようになりました。

フェーズ5(API呼び出し)は変わりません。

全体をつなげるとこうなります。

実装でのポイント

上記のフローを実現するために、実装で気をつけるポイントを整理しておきます。私はAWS CDKをよく使うので、IaC部分はCDKを使う前提です。

Cognitoアプリケーションクライアント作成

シークレットを持たないパブリッククライアントとして作りましょう。

this.adminUsageClient = this.userPool.addClient('AdminUsageClient', {
  userPoolClientName: `<クライアント名>`,
  generateSecret: false, // シークレットは不要
  supportedIdentityProviders: [
    cognito.UserPoolClientIdentityProvider.custom(samlProvider.providerName),
  ],
  oAuth: {
    flows: { authorizationCodeGrant: true, implicitCodeGrant: false },
    scopes: [
      cognito.OAuthScope.OPENID,
      cognito.OAuthScope.EMAIL,
      cognito.OAuthScope.PROFILE,
    ],
    callbackUrls: ['http://localhost:53682/callback'],
  },
  enableTokenRevocation: true,
  accessTokenValidity: cdk.Duration.hours(1),
  idTokenValidity: cdk.Duration.hours(1),
  refreshTokenValidity: cdk.Duration.days(30),
});

1点だけ、L2コンストラクトだけでは無理な設定がありました。
明示的な認証フローを ALLOW_REFRESH_TOKEN_AUTH だけに絞るため、エスケープハッチ(L2の裏にあるL1リソースを直接触る手法)を使っています。

const adminUsageClientResource = this.adminUsageClient.node
  .defaultChild as cognito.CfnUserPoolClient;
adminUsageClientResource.explicitAuthFlows = ['ALLOW_REFRESH_TOKEN_AUTH'];

コンソールで見ると、これだけが設定される形になります。
image.png

AgentCore Gateway

AgentCore Gateway側の変更は2つです。discoveryUrlを自作ProxyからCognito管理のものへ差し替え、allowedClientsに固定クライアントを設定します。

ディスカバリーURLを自前で組み立てるとするとこんな感じ。もしかしたらCDKで取れるかもしれませんが。

const cognitoDiscoveryUrl = cdk.Fn.join('', [
  'https://cognito-idp.',
  this.region,
  '.amazonaws.com/',
  props.userPool.userPoolId,
  '/.well-known/openid-configuration',
]);

// AgentCore Gateway(CfnGateway)の認可設定
customJwtAuthorizer: {
  discoveryUrl: cognitoDiscoveryUrl,
  allowedClients: [props.sampleClient.userPoolClientId],
  allowedScopes: ['openid', 'email', 'profile'],
},

この設定にした後、許可リストにない別のclient_idで認証したClaude Codeからは、接続が認証未完了(Needs authentication)のまま通らないことも確認しました。

利用者の接続手順

利用者はクライアントID・AgentCore Gateway URL・コールバックポートを一緒に登録します。

claude mcp add \
  --transport http \
  --client-id "<クライアントID>" \
  --callback-port 53682 \
  <Claude Codeに設定するMCPサーバー名> \
  <AgentCore Gateway URL>

--transport httpの設定によってリモートMCPサーバーとして設定します。クライアントIDはCognitoアプリケーションクライアントのIDを設定します。

コールバックポートについては、Cognitoアプリケーションクライアントに登録したものと同じものを指定します。
今回はhttp://localhost:53682/callbackを登録しているので、利用者は全員--callback-port 53682を指定します。

あとはClaude Codeを起動して /mcp から認証すれば、会社のSSOログイン画面に飛ばされて完了です。

まとめ

DCRと事前登録、それぞれ場面に応じて使い分けてあげるのが大事ですね。
あとClaude Codeがアップデート多すぎて追いきれないのは大変ですが、こういった基礎基本のところを理解できていれば振り回されずについていけそうでいいですね。

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?