
Claude CodeからMCPサーバーへOAuth接続する方法
社内向けの MCP サーバーを建てて、Claude Code から社内 API を叩きたい。認証は当然、会社ですでに使っている Cognito に寄せたい——そう考える方は多いのではないでしょうか。
この記事は、MCP 認可仕様(2025-06-18)に沿って自前の認可サーバーを設計するなら、どう組むべきかをまとめたものです。結論を先に言うと、キモは 1 つだけです。
Cognito をそのまま Authorization Server (AS) にするのではなく、自前の AS を建てて Cognito を「upstream IdP」として裏に隠す(proxy AS パターン)。
これで MCP クライアント側の DCR 要求と、企業 IdP(Cognito)側の制約を両立できます。
この記事でわかること
- MCP クライアントが DCR(動的クライアント登録)を要求してくる理由と、Cognito をそのまま AS にできない理由
- 自前 AS を建てて Cognito を裏に隠す proxy AS パターンの設計ポイント
- 二重に走る PKCE と、refresh token rotation の注意点
- DCR を開放する構成で必ず塞がなければならない攻撃筋
- 設計・構築時に踏みやすい落とし穴 3 つ
OAuth の基礎用語も次の節で定義してから進めるので、ふんわりとしか知らなくても大丈夫です。

この記事で使う用語
- 認可コードフロー(Code Flow) — ①クライアントがユーザーをログイン画面に送り、②ログインすると短命の「認可コード」がクライアントに戻り、③クライアントがそのコードを裏側で AS に渡してアクセストークンと交換する、という手順です。トークンをいきなり渡さず一度コードを挟むことで、URL やブラウザ履歴への露出を防ぎます。
- PKCE(Proof Key for Code Exchange) — 上のコードフローの拡張で、認可コードを横取りされても悪用されないようにする仕組みです。クライアントは最初にランダムな秘密(verifier)を作り、そのハッシュ(challenge)だけをログイン開始時に送ります。コードをトークンに交換するときに元の verifier を提示させ、AS が両者の対応を照合します。verifier を知らない攻撃者はコードを盗んでも交換できません。この記事では、これを「二重に」使うのがポイントになります。
- AS(Authorization Server / 認可サーバー) — 「誰にどのトークンを発行するか」を決めるサーバーで、
/authorize(ログイン開始)と/token(トークン発行)を提供します。この記事の主役は自前で建てる AS です。 - IdP(Identity Provider) — 「この人は誰か」を認証する主体で、この記事では Cognito です。Cognito は単体で AS も兼ねられますが、ここではあえて AS=自前サーバー / IdP=Cognito と役割を切り分けます。この構成が proxy AS パターンです("upstream IdP" は「AS の裏側に控える IdP」の意味)。
- DCR(Dynamic Client Registration) — クライアントが接続時に「自分はこういうアプリです」と AS に自己登録する仕組みです。MCP クライアントはこれを使います。
以降、単に「AS」と書いたら自前で建てる認可サーバー、「IdP」または「Cognito」と書いたら認証を委譲する先、と読んでください。
最初の設計判断 — Cognito を直接 AS にはできない
MCP クライアント(Claude Code など)は、接続のたびに DCR で「自分はこういうクライアントです」と AS に登録しにきます。事前に client_id を発行して配っておく、という前提になっていないんですね。
ところが Cognito は DCR に対応していません。 クライアントはコンソールなり IaC なりで事前に手作業で作るもの、という設計思想です。ここで「Cognito をそのまま AS にする」という素直な発想が破綻します。
そこで選択肢になるのが proxy AS パターンです。
- MCP サーバー自身を Authorization Server にします(
/authorize・/token・/registerを自前で実装) - Cognito は AS の座からは外し、役割を「人間がログインしていることを確認する装置(upstream IdP)」に限定します
役割を整理すると、トークンを「発行する」のは自前 AS、発行されたトークンを受け取って「検証する(aud・exp などを確認する)」のは MCP(リソース)サーバーです。Cognito は「人間が誰か」の確認に徹し、トークンの発行・検証には関与しません。この責務分担が proxy AS の肝で、クライアントから見れば「自前 AS としか喋っていない」ことになり、Cognito の存在は意識されません。
設計の全体像
以下、意味のまとまりで 5 つのステップに分けて説明します。具体的なコードは載せず、設計の勘所だけを言葉で追います。
1. Discovery(RFC 8414 / 9728)
MCP クライアントが最初に見にくるのが metadata エンドポイントです。
/.well-known/oauth-authorization-server→ RFC 8414(AS Metadata)/.well-known/oauth-protected-resource→ RFC 9728(Protected Resource Metadata)
AS メタデータで最低限そろえるのは issuer(自前 AS の基底 URL)、authorization_endpoint / token_endpoint、registration_endpoint、code_challenge_methods_supported(S256)、grant_types_supported(authorization_code・refresh_token)あたりです。いずれも自前 AS 自身のエンドポイントを指すのがポイントで、ここを Cognito に向けてしまうと proxy の意味がなくなります。
なお oauth-protected-resource(PRM)は AS メタデータとはフィールドセットが別物で、「このリソースは何で、どの AS が認可を担うか」を示す resource や authorization_servers などを、仕様に沿って別途用意します。
ここの URL やフィールドが 1 つでもズレていると、クライアントは黙って接続を諦めます。
2. authorize / callback(proxy AS の心臓部)
いちばん頭を使うのがここです。PKCE が二重に走ります。 しかも「challenge を保存する側」と「verifier を検証する側」が違うので、対応関係を頭に入れておかないと必ず混乱します。
- PKCE A:MCP クライアント ↔ 自前 AS。challenge を作るのはクライアント、検証するのは自前 AS
- PKCE B:自前 AS ↔ Cognito。challenge / verifier を両方作るのは自前 AS、検証するのは Cognito
図中の「登録済み redirect_uri との exact match 照合」と「同意画面」は、後述のとおり省略できない必須要件です。

要注意なのは、A と B の検証タイミングがフェーズをまたいでズレている点です(図の ★ 印)。B は /callback の中で Cognito が検証しますが、A の検証は最後の /token まで持ち越されます。そのため自前 AS は challenge_A を /authorize 受信時から /token 到達時まで、内部 code に紐づけた状態で保持し続ける必要があります。「A も callback で検証すればいい」と勘違いすると、クライアントが送ってくる verifier_A を受け取る前に検証しようとして詰みます。
また当然ですが、verifier(A も B も)は絶対に外部へ出さないでください。verifier_B はリダイレクト URL やレスポンスに乗せず、サーバー側ストレージ(セッション or 短命な KV)にだけ持ちます。ブラウザを経由するリダイレクトに載せてよいのは challenge 側だけ、という PKCE の大原則を二重フローでも崩さないことが重要です。
もう 1 つ、RFC 8707(Resource Indicators)の resource パラメータにも触れておきます。MCP 認可仕様(2025-06-18)では、クライアントが /authorize・/token に「どのリソースに対するトークンが欲しいか」を示す resource を送ることが MUST とされています。自前 AS はこれを受け取り、発行するトークンの aud に反映します。「このトークンはこのリソース専用」という結びつきがトークン自体に刻まれ、別のリソースへの使い回しを防げます。ただしこの手のトークン内クレームは、刻むだけでは意味がなく、受け取るリソースサーバーが必ず検証して初めて機能します。 発行と検証はセットです。
3. token — 内部 code を自前トークンに交換
設計上のポイントは refresh rotation + replay 検知です。refresh token は 1 回使ったら失効させて新しいものに差し替え(rotation)、同じ refresh token が二度使われたら「盗まれて再利用された」とみなして系列ごと失効させます。系列の識別には chain_id を持たせます。
渡ってきた refresh token は次の順で捌きます。
- 保存済みレコードを引く。 見つからなければ「存在しない token(失効済み or 偽造)」として拒否し、監査ログに残す。
- 「使用済み」フラグが立っていれば replay。 同じ系列 ID を持つトークンを丸ごと失効させ、replay 検知のイベントを残して拒否する。
- 未使用なら、まず使用済みフラグを立てて永続化してから、同じ系列 ID を引き継いだ新しいトークン一式を発行する。

ここで効いてくる設計判断が 2 つあります。
1 つは、「存在しない token」と「使用済みの再利用」を別分岐にすることです。「見つからない、または使用済みなら拒否」と 1 条件にまとめると、レコードが無いのに系列 ID を参照して実行時エラーになりますし、「偽造・失効済み」と「replay 検知」はログを分けたほうが後から追えます。
もう 1 つは、使用済みフラグを「新トークン発行より前」に確定させることです。発行してからフラグを立てる順にすると、並行リクエストで同じ token が二重に通り抜ける窓ができます。「先に使用済みにする → それから発行」の順序が肝です。
なお正規ユーザーでも「新しい refresh token を受け取る直前に通信が切れ、古い token を再送する」ことは普通に起こり、上の設計では系列失効に巻き込まれます。厳しすぎる場合は直前 1 世代だけ短い猶予期間(grace window)を設ける手がありますが、広げるほど replay の検知精度が下がるので、盗難検知と UX のどちらを取るかは扱うデータの機微度で決めてください。
4. DCR(/oauth/register, RFC 7591)
DCR エンドポイントは誰でも叩けるので、運用上は想定クライアントかどうかを識別名などで整理しておきたくなります。
ただし重要な注意があります。クライアントが名乗る識別名は自己申告値なので、これはセキュリティ境界ではなく「ノイズフィルタ」にすぎません。 誰でも任意の名前を名乗れる以上、認可の砦にはなりません。
本当の防御は、Cognito のログイン(人間の認証)と、後述の redirect_uri の厳密な検証・クライアントごとの同意が担います。「DCR に何らかのチェックがあるから安全」と過信しないでください。なお外形的な防御として、/authorize や /token にはレート制限をかけておきます。
5. 監査ログ
ログイン成功・refresh 成功・無効な token での要求・replay 検知、このあたりは最低限記録しておきたいところです。
1 つ設計上の小ネタがあります。ログ書き込みの失敗でトークン発行そのものを壊してはいけない、という二重防衛の考え方です。監査ログの書き込みは try/except で囲み、失敗しても発行だけは必ず通す。失敗はアプリログに残して後から気づけるようにしておきます。監査基盤の一時障害でログインできなくなるのは本末転倒なので、「ベストエフォートで残す、ただし本流は止めない」が原則です。
最重要: redirect_uri の検証とクライアントごとの同意
proxy AS を作るなら、他のどの対策よりも先にここを固めてください。DCR を開放している以上、次の攻撃筋を必ず塞ぐ必要があります。
- 攻撃者が、自分の管理する
redirect_uriでクライアントを登録する(DCR は誰でも叩ける)。 - 攻撃者が、社内の誰かにその
redirect_uriを仕込んだ authorize URL を踏ませる。 - 被害者のブラウザに Cognito の SSO セッションが生きていれば、無操作でログインが成立してしまう。
- 認可コードが攻撃者の
redirect_uriに飛ぶ。PKCE の challenge は攻撃者自身が作っているので防御にならず、攻撃者は自分の verifier でトークン交換を完了できる → 被害者のトークンを奪取。
ここで効かないものをはっきりさせておきます。クライアントが名乗る識別名(client_name など)は、この攻撃に対して無力です。 自己申告なので攻撃者も同じ名前を名乗れますし、authorize の経路がクライアント登録情報を厳密に参照していなければ登録すら迂回されます。PKCE も、challenge を攻撃者が握っている以上この筋では守りになりません。
塞ぎ方は 2 つあり、両方とも必須です。
- (a) redirect_uri を厳密に検証する。 DCR で受け付ける
redirect_uriを loopback(127.0.0.1/localhost)に限定し(MCP のローカルクライアントは通常これで足ります)、authorize 時には「登録済みクライアントを引いて、その登録値と完全一致(exact match)で照合する」。前方一致やホスト一致では不十分です。authorize が client を参照せずに任意のredirect_uriを受けてしまうと、この検証自体が成立しません。 登録は register 側、検証は authorize 側、と実装が分かれるので、authorize がクライアントストアを引いていることを必ず確認してください。 - (b) クライアントごとに同意画面を挟む。 動的登録された各クライアントについて、third-party AS(Cognito)へ転送する前に、ユーザー本人に「このクライアントにアクセスを許可しますか」と明示的に同意を取る。これは MCP 認可仕様(2025-06-18)の Security Considerations が MUST として要求している要件です。static な client ID を使う proxy が DCR クライアントを転送する構成は、まさにこの条項の対象です。同意を挟めば、被害者の無操作で認可が完了する経路(ステップ 3)が断たれます。
この 2 つは「あれば望ましい」ではなく、proxy AS の認可の一次防御です。ここが無い状態では、以降の id_token 検証やトークン寿命をどれだけ丁寧に設計しても入り口が開いたままになります。揃うまでは、その AS を外部から到達できる場所に置かないでください。
設計上、議論を呼びそうな点
id_token の署名検証は省略してよいか
/callback で Cognito から受け取った id_token について、署名検証を省略して decode してよいケースがあります。一見アウトに見えますが、仕様上の裏付けがあります。
この id_token は、自前 AS が Cognito の token endpoint と直接 HTTPS で通信して受け取ったものです。OpenID Connect Core 1.0 の 3.1.3.7 (ID Token Validation) では、「token endpoint から TLS 越しに直接取得した id_token については、そのサーバーを信頼できる場合に限り署名検証を省略してよい」と明示的に許容されています。 TLS が担保するのは「本物の Cognito と、中間者なしで通信できたこと」であり、その経路で直接受け取ったトークンなら署名という第二の真正性チェックは必須ではない、という判断です。
逆に言うと、省略が許されるのは「自分が token endpoint を叩いて、そのレスポンスとして直接受け取った」ケースだけです。第三者を経由して渡ってくる JWT(クライアントが Authorization ヘッダで送ってくる access token など)は、必ず署名を検証してください。 こちらは TLS で「直接もらった」保証がないので、JWKS で公開鍵を取得して署名・iss・aud・exp を確認します。この線引きを明示しておかないと「署名検証をサボっている」と誤解されるので、コメントなどで根拠を残しておくとよいでしょう。
トークン寿命の設計
以下は一例で、扱うデータや運用に合わせて調整する前提の値です。
- access token: 短め(例: 15 分程度) — 漏洩時の被害窓を最小化します。MCP は自動で refresh できるので、短くても UX を損ないにくいです
- refresh token: やや長め + rotation(例: 数日〜1 週間) — 使うたびにローテーションし replay を検知します。長さは「毎日使う社内ツールで再ログイン頻度が許容範囲に収まるか」で決めます
数字は環境次第ですが、「access は短く、refresh は rotation 前提でやや長く」という方針自体は流用できるはずです。
踏みやすい落とし穴 3 選
構築時に詰まりやすいポイントです。同じ症状で検索してたどり着いた人は、この章だけ読めば大丈夫です。
① DCR クライアントの識別名まわりでつまずく
MCP クライアントが DCR で名乗る識別名の形式はクライアント実装ごとに決まっており、サーバー側の想定とズレると登録・接続の段階で弾かれます。厄介なのは、この識別名がクライアント側の設定ファイルの記述と連動して決まる場合があること。設定ファイルに書いた名前がそのまま登録リクエストに反映されるため、一見ただの設定値がサーバー側の受け入れ判定に直接効いてきます。
DCR のクライアント識別まわりは「クライアント側の設定」と「サーバー側の受け入れ条件」をセットで管理してください。ただし前章のとおり、この識別名を『セキュリティ境界』として頼るのは危険です。
② ALLOW_REFRESH_TOKEN_AUTH is not a permitted ExplicitAuthFlow when refresh token rotation is enabled
Cognito で refresh token rotation を有効にすると、ExplicitAuthFlows に ALLOW_REFRESH_TOKEN_AUTH を同居させられず、デプロイが落ちます。
一見「どちらを取るか」の選択に見えますが、そうではありません。proxy AS では refresh 管理をアプリ層(自前 AS)で持つので、Cognito 側の refresh 機能はそもそも使いません。つまり Cognito 側の refresh 設定は丸ごと不要です。このエラーに当たったら、「設計が Cognito の refresh に依存していないか」を確認する機会だと捉えるとよいでしょう。依存していないなら、責務分離が正しくできている証拠でもあります。
③ Login pages unavailable(Managed Login v2)
Cognito の ManagedLoginBranding は App Client ごとに紐付けが必要です。既存の別用途の App Client にブランディングを設定してあっても、MCP 用に新しく作った App Client には紐付いていないので、Hosted UI が表示されません。App Client を増やしたら、その App Client にもブランディングを明示的に紐付ける——忘れがちです。
おわりに
proxy AS を建てるなら——強調しておきたいのですが——redirect_uri の厳密な検証(loopback 限定 + 登録値との完全一致)と、クライアントごとの同意画面を、他の何よりも先に用意してください。 DCR を開放する構成は、これらが無いと認可コードの奪取に直結します。便利さの前に、まず入り口を閉めるのが順序です。
最後に注意書きを 1 つ。MCP の認可仕様はまだ動いています。 RFC の要求セットや推奨フローは今後変わる可能性が高いので、この記事も「2026 年前半時点のスナップショット」として読んでください。
同じところで詰まっている人の時間が、少しでも減れば幸いです。
参考仕様・ドキュメント
URL は執筆時点のものです(特に AWS のドキュメントはパスが変わりやすいため、リンク切れの場合はタイトルで検索してください)。
- RFC 8414 — OAuth 2.0 Authorization Server Metadata
https://datatracker.ietf.org/doc/html/rfc8414 - RFC 7591 — OAuth 2.0 Dynamic Client Registration Protocol
https://datatracker.ietf.org/doc/html/rfc7591 - RFC 9728 — OAuth 2.0 Protected Resource Metadata
https://datatracker.ietf.org/doc/html/rfc9728 - RFC 7636 — Proof Key for Code Exchange (PKCE)
https://datatracker.ietf.org/doc/html/rfc7636 - RFC 8707 — Resource Indicators for OAuth 2.0(
resourceと audience の結びつけの根拠)
https://datatracker.ietf.org/doc/html/rfc8707 - OpenID Connect Core 1.0(特に 3.1.3.7 ID Token Validation)
https://openid.net/specs/openid-connect-core-1_0.html - Model Context Protocol の Authorization 仕様
https://modelcontextprotocol.io/ - AWS Cognito 公式ドキュメント(Hosted UI / Managed Login、refresh token rotation、App Client 設定)
https://docs.aws.amazon.com/cognito/


