1. この講義の内容と含まれないもの
本講義では次のことを扱います。
- MCP Client、MCP Server、MCP Auth Server の間の「信頼の三角形」を頭の中に描けるようにし、リソースの所有者としてこの三角形の「上」に立つ人間のユーザーの位置づけを理解します。
- フローを言語化します: 誰が誰にトークンを送るのか、ユーザーはどこでログインするのか、そしてなぜMCPサーバーはユーザーのパスワードを決して見ないのか。
- これを私たちの Next.js/MCP のバックエンドと、今後設定する Keycloak/Auth0 に結びつけます。
今日やらないこと:
- Keycloakでチェックボックスをクリックして具体的な IdP を設定することはしません。
- 完全なJWT検証やイントロスペクションは書きません — それらは次の講義(Auth Server と、保護されたリソースとしての MCP Server)で扱います。
ここでの目標は、紙に ChatGPT、あなたのサーバー、Auth0/Keycloak の間の矢印を描き、「ログインはどこか」「トークンはどこか」「データはどこか」を詰まらずに説明できるようになることです。
2. 信頼の三角形: MCP Client, MCP Server, MCP Auth Server
登場人物から始めましょう。技術的な「信頼の三角形」は MCP Client、MCP Server、MCP Auth Server で構成されます。ユーザー(User)は別のロールであり、リソースの所有者としてこの三角形の上に立ち、アクセスに同意を与えます。MCP と Apps SDK の文脈では、このアーキテクチャはかなり明確に形式化されています。
User (Resource Owner)
画面の向こう側にいる人です。ユーザーは:
- ChatGPT にアクセスします。
- 「私の注文を見せて / 私のギフトリストを見せて」といったリクエストを書きます。
- あなたのサービスのアカウントを ChatGPT に「リンク」することに同意します。
重要なのは、リソース(注文履歴、プロフィール、ギフトリスト)の所有者はユーザーであり、アクセスへの同意を与えるのもユーザーだという点です。
MCP Client
ここでは次のものを指します。
- Apps SDK を備えた ChatGPT;
- 場合によっては MCP Jam Inspector(デバッグ時)。
MCP Client ができること:
- あなたの MCP サーバーのメタデータを読み取る(.well-known 経由)。
- ユーザーのブラウザで OAuth フローを開始する。
- MCP ツールを呼び出す際にトークンを保存・添付する。
MCP Client は public client だという点を忘れないでください。client_secret は保持せず、Auth Server とはパブリックな SPA として通信します: Authorization Code + PKCE。
MCP Server (Resource Server)
これは MCP を実装するあなたのバックエンドです:
- ChatGPT と接続を確立する。
- ツール(tools)、リソース、プロンプトを公開する。
- 各ツール呼び出しでヘッダー Authorization: Bearer <token> を確認する。
- トークン(署名、exp、aud、scope)を検証し、問題なければビジネスロジックを実行する。
重要なポイント: MCP サーバーはログイン処理を行いません。パスワードを見ず、ログインフォームを描画せず、「メールを確認してください」といったメールを送りません。Auth Server が発行した暗号学的に署名済みのトークンだけを信頼します。
MCP Auth Server (Authorization Server / IdP)
これは認証と認可のための別サービスです: Keycloak、Auth0、Ory Hydra+Kratos、Okta、Cognito、Azure AD など。
Auth Server の役割:
- ログインUI(メール/パスワード、SSO、2FA)。
- ユーザーアカウントの保管。
- トークンの発行(access token, refresh token)。
- OAuth/OIDC メタデータの公開(/authorize、/token、jwks_uri、/registration など)。
MCP に対しては、public client 向けの OAuth 2.1(PKCE S256、dynamic client registration など)をサポートする必要があります。
役割のまとめ表
| 誰 | 役割 | しないこと |
|---|---|---|
| User | ログイン/パスワードを入力し、データへのアクセスに同意する | MCP Server と直接やり取りしない |
| MCP Client (ChatGPT/Jam) | OAuth を開始し、トークンを保持し、MCP ツールを呼び出す | パスワードやトークン署名は検証しない |
| MCP Server | トークンを検証し、ツールのビジネスロジックを実行する | ログインフォームを描画せず、パスワードを保存しない |
| MCP Auth Server | ユーザーをログインさせ、トークンを発行する | あなたの MCP ツールやそのビジネスロジックを知らない |
もし頭の中でこれらが「何でもやる大きなサーバー」として混ざっていたなら、ここで切り分けましょう。
3. フローの全体像: 「トークンなし」から保護されたツール呼び出しまで
ここからメッセージフローを見ていきます。MCP の仕様ではこのプロセスを “The Flow” と呼びます: discovery → redirect → code → token → authorized calls。
ステップ0. トークンなしで保護されたツールを呼び出そうとする
ユーザーが「保存したギフトアイデアを見せて」と書きます。
MCP Client としての ChatGPT はこう判断します: 「そのためには私たちの MCP サーバーの getUserGiftLists ツールを呼び出す必要がある」。ユーザーはまだログインしていないので、トークンなしで呼び出します。
あなたの MCP サーバーは:
- Authorization ヘッダーがない、または不正であることを検知する。
- 401 Unauthorized を返し、ヘッダー WWW-Authenticate: Bearer resource_metadata="https://api.giftgenius.com/.well-known/oauth-protected-resource" を追加して、保護されたリソースのメタデータ(resource metadata、後述)へのリンクを示します。
概ね次のようになります(ロジックのみ、完全なHTTPではありません)。
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://api.giftgenius.com/.well-known/oauth-protected-resource"
ChatGPT はこのヘッダーを見て、「なるほど、このリソースは OAuth で保護されている。OAuth フローを実行してアカウントをリンクしよう」と理解します。
Discovery: .well-known/oauth-protected-resource
MCP Client は、次にあなたのサーバーからメタデータを取得します。
GET /.well-known/oauth-protected-resource
サーバーは、リソース識別子と、トークンを取得すべき認可サーバーのリストを含む JSON ドキュメントで応答します。
最小の例(詳細設定は後で行います。ここでは概念が重要です)。
{
"resource": "https://api.giftgenius.com",
"authorization_servers": [
"https://auth.giftgenius.com"
],
"scopes_supported": ["gifts.read", "gifts.write"]
}
ここで:
- resource — あなたのリソースの正規 ID。後でトークン発行時に audience または resource として使用されるべきもの。
- authorization_servers — ChatGPT がトークンを要求できる Auth Server のリスト。
- scopes_supported — あなたの MCP サーバーが理解する「権限」。
Authorization Request: Auth Server へのリダイレクト
メタデータを受け取ったら、MCP Client は Auth Server に向かいます。ユーザーのブラウザで次のURLを開きます。
GET https://auth.giftgenius.com/authorize
?response_type=code
&client_id=chatgpt-giftgenius
&redirect_uri=... (MCP Client のコールバックURL)
&code_challenge=...
&code_challenge_method=S256
&scope=openid gifts.read
&resource=https://api.giftgenius.com
ユーザーは次を行います。
- 見慣れたログイン画面(例: Keycloak や Auth0)を見る。
- ログイン/パスワードを入力し、2FA を通過する。
- ChatGPT が自分のギフトリストを読めることに同意する(scope gifts.read)。
コードからトークンへ: PKCE で交換
ログインが成功すると、Auth Server は code を付けてユーザーを MCP Client にリダイレクトします。MCP Client は:
- /token に対して POST を行い、
- code と、先ほどの code_challenge に対応する code_verifier を渡します。
Auth Server は PKCE を検証します: code_verifier をハッシュし、最初の code_challenge と比較します。問題なく、フローを開始したクライアントと同一であると確認できれば:
- 短寿命の access_token(通常は JWT)を発行し、
- その中に次を含めます:
- sub — Auth Server におけるユーザーID。
- aud または resource — あなたの MCP サーバー。
- scope — 許可された操作(gifts.read、openid など)。
認証済みリクエスト: トークン付きで MCP ツールを呼び出す
これで MCP Client は、次のヘッダーを付けてツールを再度呼び出す準備が整いました。
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
MCP サーバーは次を行います。
- トークンの署名を(Auth Server の JWK で)検証、またはイントロスペクションを行う。
- 有効期限(exp)を確認する。
- aud / resource を確認し、そのトークンが本当に https://api.giftgenius.com 向けに発行されたものかを検証する。
- scope を見て、getUserGiftLists を呼び出して良いかを判断する。
その後、サーバーは自分のDBで対応する userId を使ってデータを取りに行き、個人のギフトリストを返します。
ここまで話してきたのはネットワーク上のフロー、すなわちトークンがどのように取得され、MCP サーバーまで運ばれるかだけです。次に重要なのは、トークン内の sub などの claim が、どのようにあなたのDBでの具体的な userId に変換されるかです — ここでアイデンティティブリッジの出番となります。
4. Identity Bridge: ChatGPT のユーザーがあなたのDBの userId になるまで
アーキテクチャで最も興味深い部分が「アイデンティティブリッジ」です。MCP の仕様では明確に強調されています: MCP サーバーは ChatGPT のユーザーについては知らず、Auth Server のトークンに含まれるデータに依拠します。
概略は次のとおりです。
flowchart TD User[ChatGPT のユーザー] -->|Login/SSO| Auth[Auth Server] Auth -->|JWT: sub, email, tenant| MCP[MCP Server] MCP -->|userId/tenantId| DB[(あなたのDB)]
手順は次のとおりです。
まず、Auth Server は自分のユーザーを把握しています。user、email、id、必要なら tenant や roles といったエンティティを持っています。ログインが成功すると、これらの情報を(claims として)トークンに入れます。
{
"sub": "auth0|abc123",
"email": "user@example.com",
"given_name": "Alice",
"https://giftgenius.com/tenant": "tenant-42",
"scope": "openid gifts.read",
"aud": "https://api.giftgenius.com"
}
次に、MCP Server はトークンを検証する際にこれらの claim を取り出し、自分の世界では誰なのかを決めます。たとえば:
- sub がすでにテーブル User.authProviderId にあるなら、対応する userId を取得する。
- なければローカルのレコードを作成して(オンザフライ・プロビジョニング)紐づける。
MCP サーバー側の典型的な TypeScript コード断片(単純化、署名検証は省略)は次のようになります。
type TokenClaims = {
sub: string;
email?: string;
scope?: string;
};
async function mapClaimsToUserId(claims: TokenClaims): Promise<string> {
const user = await db.user.findUnique({ where: { authSub: claims.sub } });
if (user) return user.id;
const created = await db.user.create({
data: { authSub: claims.sub, email: claims.email ?? null }
});
return created.id;
}
最後に、手元の userId を使って、MCP サーバーは必要なもの(ギフトリスト、注文履歴、設定、料金プランなど)を取得します。
このように、Auth Server は外部世界(ChatGPT、Google、SSO)とあなたの内部世界(注文DBの customer_id など)をつなぐ「橋」となります。
5. Auth Server と MCP Server を分離すべき理由
「MCP サーバーが自分でログイン画面も出してトークンも発行すればいいのでは」という誘惑があるかもしれません。形式的には可能(サーバー内部にミニ IdP を組み込む)ですが、アーキテクチャとしては望ましくありません。理由は現実的です。
第一に、セキュリティとスケーラビリティです。Auth Server は重厚なマシンです: 2FA、ソーシャルログイン、パスワードポリシー、アカウントロック、アカウント復旧、サインイン監査、場合によっては認証取得。これを各マイクロサービス(各 MCP サーバー)で毎回実装するのは地獄と PCI-DSS の道です。Keycloak/Auth0 に委譲してトークン検証だけに集中する方が遥かに簡単です。
第二に、クライアントの置き換え容易性です。今日は ChatGPT だけでも、明日は Claude Desktop、自分の Next.js フロントエンド、モバイルアプリを接続するかもしれません。これらはすべて同じ Auth Server と同じ OAuth 2.1 スキームを利用でき、あなたの MCP サーバーはトークン検証を続けるだけです。新しいクライアントごとにビジネスロジックを書き換える必要はありません。
第三に、コードの清潔さです。理想的な MCP Server は:
- /.well-known/oauth-protected-resource を公開でき、
- Bearer トークンを検証して userId、scopes、tenant を取り出せ、
- ビジネスツール(orders, gifts, profiles)を実装します。
ログインのUIロジック — フォーム、レイアウト、ソーシャルログイン — は Auth Server に置き、バックエンドを煩雑にしないようにします。
6. 学習用アプリ GiftGenius ではどうなるか
コースを通じて作っているアプリに戻りましょう。次の構成を想定します。
- ギフトを選定できるウィジェット(Apps SDK)付きの ChatGPT App「GiftGenius」。
- ツールを提供する Node/Next.js 上の MCP サーバー:
- searchGifts — 匿名、ログイン不要。
- getSavedGiftLists — 個人向け、認証が必要。
- Auth Server(後で Keycloak/Auth0)で、各ユーザーにアカウントがある。
匿名ユーザーとログイン済みユーザーのシナリオ
ユーザーが「兄に贈る30歳向けの、ボードゲーム好きのためのプレゼントを探して」と言うだけなら、私たちの App は:
- 匿名ツール searchGifts を呼び出し、
- UI でおすすめを提示できます。
この場合は:
- トークンは不要です。
- MCP サーバーは(あなたのカタログや外部APIなどへの)検索を実行するだけです。
ユーザーが「これを私のリストに保存して」や「私の保存済みアイデアを見せて」と言った瞬間、モデルは保護されたツール getSavedGiftLists を呼び出すことにします。サーバーは 401 と WWW-Authenticate(resource_metadata 付き)で応答。ChatGPT は「GiftGenius アカウントをリンク」する OAuth ウィザードを起動し、ユーザーをログインに通し、トークンを取得します。
以降、保護された呼び出しのたびに:
- MCP Server は Authorization: Bearer ... を確認し、
- トークンから userId を取り出し、
- その userId でデータをフィルタします。
これにより、次のことが可能になります。
- ユーザーごとのデータ分離。
- 注文履歴やお気に入りの安全な表示。
- コマース機能の実装(コースの後半)。
バックエンドのアーキテクチャ: middleware + ツールハンドラー
実装上、Node/Next.js のコードでは「認証ミドルウェア → ツールのビジネスハンドラー」というチェーンにすることが多いです。ツールハンドラーの講義で、user_id、トークン、設定といったコンテキストを渡すべきだと強調しました。
コード断片は次のようになります。
// auth-context.ts
export type AuthContext = {
userId: string | null; // 匿名呼び出しでは null
scopes: string[];
};
すべての MCP エンドポイントに適用するミドルウェア:
// mcp-auth-middleware.ts
export async function buildAuthContext(req: Request): Promise<AuthContext> {
const header = req.headers.authorization || "";
const token = header.replace(/^Bearer\s+/i, "");
if (!token) return { userId: null, scopes: [] }; // 匿名ユーザー
const claims = await verifyAndDecodeToken(token); // トークンの検証
const userId = await mapClaimsToUserId(claims);
const scopes = (claims.scope || "").split(" ");
return { userId, scopes };
}
ツールハンドラーはこのコンテキストを受け取ります。
// tools/getSavedGiftLists.ts
export async function getSavedGiftLists(_args: {}, ctx: AuthContext) {
if (!ctx.userId) throw new Error("User must be authenticated");
return db.giftList.findMany({
where: { ownerId: ctx.userId }
});
}
要点は、ツールハンドラーは OAuth や PKCE を何も知らないということです。単に「当たり前の」userId とともに動作します。すべての OAuth の魔法は、その手前(MCP クライアントと Auth ミドルウェア)に隠されています。
7. ビジュアル図解: Client・Server・Auth の共存
第3章でフローを文章で段階的に説明しました。時には、何度も説明するより一度図で示す方がわかりやすいので、同じやり取りを2つの図で示します。
相互作用の骨格(The Triangle of Trust)
flowchart TD U[User] -->|1. Login / Consent| A[MCP Auth Server] U -->|2. チャットする| C["MCP Client (ChatGPT)"] C -->|3. OAuth Flow| A C -->|4. Bearer Token| S[MCP Server] S -->|5. Data| C
この図は次のように読みます。
まずユーザーは Auth Server を通じてログインし、本人性が確認されトークンが発行されます。MCP Client はこのプロセスを管理し、その後トークンを使って MCP サーバーにアクセスします。MCP サーバーはログイン/パスワードを見ず、トークンだけを見て許可・不許可を判断します。
リクエストからレスポンスまでの流れ
sequenceDiagram participant User participant ChatGPT as MCP Client participant Auth as Auth Server participant MCP as MCP Server User->>ChatGPT: "私のギフトリストを見せて" ChatGPT->>MCP: callTool(getSavedGiftLists)(トークンなし) MCP-->>ChatGPT: 401 + WWW-Authenticate (resource_metadata) ChatGPT->>Auth: /authorize + PKCE User->>Auth: ログイン/パスワードを入力し、同意を与える Auth-->>ChatGPT: redirect + code ChatGPT->>Auth: /token + code_verifier Auth-->>ChatGPT: access_token (JWT) ChatGPT->>MCP: callTool(getSavedGiftLists) + Authorization: Bearer ... MCP-->>ChatGPT: 個人のリストを含むJSON ChatGPT-->>User: ウィジェットに描画されたリスト
この図は、本モジュールの終わりまでに「目をつぶってでも説明できる」ようになってほしいフローです。
8. もう少し深掘り: 複数リソース、複数クライアント、DCR
このアーキテクチャの良い点は、拡張性に優れていることです。
第一に、複数の MCP サーバー(例: ギフト用と注文用)に対して、単一の Auth Server が異なる aud/resource のトークンを発行することができます。各リソースサーバーは、自分向けのトークンかどうかを必ず検証しなければなりません。そうしないと、あるサービス向けのトークンを別のサービスで受け入れてしまう「confused deputy」問題が発生します。
第二に、クライアントは複数あって構いません。
- ChatGPT App。
- あなた自身のフロントエンド。
- モバイルアプリ。
- MCP Gateway を介したパートナー統合。
いずれも次を行います。
- /.well-known/oauth-protected-resource を読む。
- Auth Server の場所を知る。
- OAuth 2.1 フローを実行する。
- トークンを取得して MCP サーバーを呼び出す。
第三に、最近の Auth Server はDynamic Client Registration (DCR) をサポートすることが増えています。これは API 経由でクライアントを動的に登録できる機能です。MCP の仕様はまさにこれを想定しており、クライアント(ChatGPT/Jam)は Auth Server の registration_endpoint に対して自動登録できます。
本モジュールで重要なのは次の点です。
- MCP Client、MCP Server、Auth Server は、標準化されたディスカバリドキュメントとトークンを介して通信する。
- バックエンドのコードに、すべてのクライアントを「ハードコード」する必要はない。
- 既存の認可モデルを壊さずにエコシステムを拡張できる。
9. MCP 認可アーキテクチャに関する典型的な誤解
エラー1: 「MCP サーバーは自分でユーザーをログインさせるべき」。
開発者がログインフォームを MCP サーバー内に組み込み、ツール経由でログイン/パスワードを送ろうとすることがあります。これは OAuth の思想を壊します。MCP サーバーは、いかなる状況でもパスワードを見てはいけません。ログインと同意は Auth Server の責務です。MCP サーバーはトークンとその claims のみを扱います。
エラー2: MCP Client と MCP Server の混同。
ChatGPT を「自分のバックエンドの一部」と捉え、秘密情報を保管したり、ChatGPT が自動でアクセス権を検証すると期待したりすることがあります。実際のところ、MCP Client は OAuth を開始し、トークンを添付するだけです。トークン検証と権限確認は MCP サーバーの仕事であり、ChatGPT の仕事ではありません。
エラー3: .env に置いた API キーで OAuth の代用をする。
ありがちなアンチパターンは、巨大な SERVICE_API_KEY を作って MCP サーバーの .env に入れ、それで解決したとみなすことです。この方式ではユーザー単位の権限分離ができず、個人データの安全な表示や購入処理もできません。すべてが「サービスの名義」で行われ、ユーザーの名義ではありません。これは ChatGPT Apps における認可の目的に真っ向から反します。
エラー4: audience と resource を無視する。
MCP サーバーが、署名が正しい任意の JWT を受け入れ、aud/resource を確認しない場合、同じ Auth Server が他サービス向けに発行したトークンであなたのツールが呼び出せてしまいます。これは OAuth のセキュリティモデルに反します。サーバーは、そのトークンが自分の resource 向けに発行されたものかを必ず検証しなければなりません。
エラー5: 認可ロジックとビジネスロジックの混在。
ツールハンドラーの中に、トークンの解析、署名検証、JWK とのやり取りなど、すべてを持ち込んでしまうことがあります。結果としてコードは脆く保守しづらくなります。より正しいのは、「トークン検証と userId へのマッピング」の層(ミドルウェア)を、「ツール本来のロジック」の層から分離し、後者には理解しやすい AuthContext を渡すことです。
エラー6: .well-known がなくても ChatGPT が「全部やってくれる」と期待する。
正しい /.well-known/oauth-protected-resource エンドポイントがなければ、MCP クライアントは Auth Server の場所や必要な scope を知ることができません。結果としてチャットは黙って「ログインできない」状態になり、開発者は空っぽのログを見つめることになります。正解は、MCP サーバーが認可要件を .well-known で明確に宣言し、クライアントがそれを読んでフローを構築することです。
エラー7: ビジネスロジックでユーザーを使い忘れる。
OAuth とトークンからの userId へのマッピングを正しく設定していても、DB クエリでそれを使わない(例えば ownerId = userId でフィルタし忘れる)ことがあります。すると、認証済みなら誰でも他人のデータを見られてしまいます。トークンの有無は第一歩に過ぎません。第二歩は常に、ビジネスコードで userId と scope を正しく使用することです。
GO TO FULL VERSION