CodeGym /コース /ChatGPT Apps /ChatGPT App に認証が必要な理由と OAuth の簡単な進化史

ChatGPT App に認証が必要な理由と OAuth の簡単な進化史

ChatGPT Apps
レベル 10 , レッスン 0
使用可能

1. なぜ ChatGPT App に認証が必要なのか

まず大事なことから: ChatGPT のユーザー ≠ あなたのサービスのユーザー

ChatGPT には ChatGPT 側のユーザーアカウントがあります。あなたのサービスには独自の userIdtenantId、ロール、課金、注文があります。両者はデフォルトで魔法のように結び付いていません。 MCP サーバーを立てていくつか tools を定義しただけでは、ChatGPT はそれらを抽象的なクライアントとして呼び出すだけです。

仮のアプリ GiftGenius — 贈り物の提案とウィッシュリスト管理を助ける ChatGPT App — を例にします。できるようにしたいこと:

  • ユーザーの保存済みギフトリストを表示する。
  • ギフトを「購入済み」や「受け取り済み」としてマークできるようにする。
  • 注文履歴を表示する(後で commerce/ACP に進むならなおさら)。

認証なしでは MCP サーバーは「この人は誰か」をそもそも知りません。最大でも、接続の技術的な識別子や、OpenAI がレート制限と識別のために与える匿名の subject が見える程度で、これは権限付与に使ってはならないと明確に警告されています。

認証 vs 認可

この二つの概念を最初に分けておくことがとても重要です。

  • 認証 (AuthN) は「この人は誰か?」に答えます。
  • 認可 (AuthZ) は「その『誰か』に何が許されているか?」に答えます。

ChatGPT App では概ね次のような流れです。

  1. まず OAuth を通じて、ユーザーがあなたの Identity Provider (IdP)(例: Keycloak/Auth0)に実際にログインしていることを確認し、そのユーザー識別子を含むトークンを受け取ります。これが認証です。
  2. 次に MCP サーバーがトークンを読み取り、sub、ロール、その他のクレームを取り出して、このユーザーが特定のツール(list_ordersdelete_profile など)を呼べるかどうかを判断します。これが認可です。

コードレベルでは(単純化すると)次のように表せます。

// MCP サーバーがユーザーについて知りたいデータ型
export interface AuthContext {
  userId: string;
  roles: string[];
}

// tool ハンドラーでの使用例
async function listGiftLists(auth: AuthContext | null) {
  if (!auth) {
    throw new Error("User is not authenticated");
  }

  // このユーザーのリストだけを DB から取得する
  return db.giftLists.findMany({ where: { ownerId: auth.userId } });
}

userId とロールがないと、正しくビジネスロジックを書けません。すべてが「みんなで 1 つの大きな共通アカウント」に堕します。

2. 「.env の API キー」方式は解決策ではない

開発者としての自然な反射があります。「API キーを作って .env に入れておけば動くはず」。そして実際、サービス間(service-to-service)の内部連携には API キーは妥当な手段です。しかし、実ユーザーと ChatGPT App が絡むと「みんなで 1 本の鍵」方式は破綻します。

初期のモジュールで、MCP から自分の backend に叩いていただけの典型的なコードを見てみましょう。

// mcp/backendClient.ts
export const backendClient = new BackendClient({
  baseUrl: process.env.BACKEND_URL!,
  apiKey: process.env.BACKEND_API_KEY!, // ChatGPT 全体で 1 つのキー
});

backend の視点では、すべてのリクエストが同じように見えます。「これは ChatGPT 連携だ」。マーシャとパーシャの区別はありません。つまり:

  • 「マイページ」を表示できない — サーバーはそれが誰のものか分かりません。
  • 権限を分けられない: 「このユーザーは閲覧のみ、こちらは購入も可」など。
  • 注文をあなたの基幹システム上の人物にひも付けられない

MCP の世界ではこれは安全面でも問題です。仕様は Streamable HTTP を用いた HTTP 認証(Bearer、API キーなど)の使用を推奨しますが、保護されたリソースへのユーザーアクセスは 1 本のサービスキーではなく、OAuth とトークンで構築するのが望ましいと強調しています。

さらに、OpenAI のポリシーの観点でも、良いアプリは本当に必要なデータだけを要求し、ユーザーに App と共有する内容をコントロールさせるべきです。これは OAuth のスコープモデルと非常に相性が良い一方、「何でもできるスーパーキー 1 本」とは全く相容れません。

ChatGPT の文脈でサービス用キーが良くない理由

サービス用 API キーが表すのは サービスの人格であって、ユーザーではありません。これはあなたの MCP サーバーから内部サービスや外部 API(OpenAI API など)を呼ぶ際の署名には使えますが、「これはヴァーシャです。彼の注文履歴を見せてください」とは言えません。

最も単純なアンチパターン:

// 悪い例: ユーザーを「だます」やり方
async function getMyOrdersFromBackend() {
  // MCP サーバーが backend の /orders/me を呼ぶ
  const res = await fetch(`${BACKEND_URL}/orders/me`, {
    headers: {
      Authorization: `Bearer ${process.env.BACKEND_API_KEY}`,
    },
  });

  // backend は "me" を人ではなく統合用サービスだとみなす
  return res.json();
}

仮に匿名の userId をリクエストボディに無理やり入れたとしても、それは結局「手作りの自転車」にすぎません。結局必要になるのは:

  • 「これが本当にそのユーザー本人だ」と backend に証明する確実な方法。
  • 特定ユーザーの権限を制限する方法。
  • 全員ではなく、特定ユーザーだけのアクセスを取り消す(revoke)仕組み。

そこで登場するのが OAuth です。

3. ミニ用語集: サインイン仕組みに求めるもの

OAuth の歴史に飛び込む前に、ChatGPT App にとって「まともな」認証システムの要件を整理しておきましょう。

必要なのは、次のような仕組みです。

  1. 外部 IdP(Keycloak、Auth0、Hydra+Kratos など)が、ログイン、メール、userId、場合によっては tenant まで、実在のユーザーを把握している
  2. その IdP が 短寿命のトークンを発行し、ChatGPT がそれを HTTP ヘッダー Authorization: Bearer <token> で安全に MCP サーバーへ渡せる。
  3. MCP サーバーが トークンを読み、署名、issuer、audience、有効期限、scopes を検証し、sub(ユーザー ID)を取り出して、それに基づいて自システムのエンティティ(accountIdtenantId)に ユーザーをマッピングできる。
  4. 同じ scopes によって 権限を細かく制御できる。あるトークンは read:gifts のみ、別のトークンは write:giftscheckout も許す、など。
  5. トークンがない、または scopes が不十分な場合、サーバーは _meta["mcp/www_authenticate"] 付きの エラーを返しChatGPT に認可 UI を表示させたりトークンを再取得させたりできる。

要するに、これらすべてができる 標準で実戦済みのプロトコルが必要です。 結論: それが OAuth 2.1 です(とその前後の兄弟たち)。

4. OAuth の簡単な進化: 恐竜から PKCE まで

ここからは仕様の細部に踏み込みすぎず、なぜ現代的パターンが重要なのかが分かる程度に、OAuth の進化をコンパクトに辿ります。

OAuth 1.0 / 1.0a: クリプト筋トレ

最初に登場したのは OAuth 1.0。これは、ユーザーパスワードを渡さずに他サービスへ自分のリソースへのアクセスを与えられるようにしました(ここまではよい)。しかし:

  • リクエスト署名が複雑でした。ほぼ全リクエストを HMAC 署名、ベース文字列の生成、パラメータ正規化など。
  • すべてのリクエストを署名し、consumer secret を保存し、正しい署名を作れる必要がありました。

現代の多くの開発者は、これらの儀式を手作業で再現したいとは思いません。

1.0a でいくつかの脆弱性は改善されましたが、全体の重さは残りました。

OAuth 2.0: 1 つの「プロトコル」ではなくフレームワーク

OAuth 2.0 は大きく様相を簡素化しました。厳密に決められた 1 つの方式の代わりに、複数のフロー (authorization code、implicit、resource owner password、client credentials など)が導入されました。柔軟性は増しましたが、実装の動物園も生みました。

利点:

  • SPA、モバイル、サーバーアプリを統合しやすくなった。
  • 役割の明確な分離が登場: Resource OwnerClientResource ServerAuthorization Server

欠点:

  • 現実では危険な「近道」が多数生まれました。フロー implicit (サーバーでのコード交換なしに、ブラウザへ直接トークンを返す方式)は安全ではないことが判明。
  • フロー password grant(クライアントがユーザーのログイン/パスワードを直接送りトークンに交換)は OAuth の思想に反しており、アンチパターンになりました。

仕様それ自体が「選択肢」を多く残したため、別の RFC やブログでベストプラクティスが多数提案される状況になりました。

OAuth 2.1: 仕切り直しでベストプラクティスを整理

OAuth 2.1 は、既にコミュニティで形成されていた ベストプラクティスの成文化です。

  • Authorization Code Flow にほぼ一本化。
  • PKCE(Proof Key for Code Exchange)の必須化。特に public クライアント (モバイル、SPA、そして… ChatGPT/MCP クライアントなど、クライアントシークレットを保持できないもの)。
  • implicit や password grant などの旧式で危険なフローは仕様から除外。
  • 短命な access token と、長いセッションには refresh token を使う推奨。

なぜ重要か?MCP と ChatGPT のエコシステムは明らかにこれらのベストプラクティスに沿っており、Apps SDK と MCP Authorization 仕様は、まさに認可コード + PKCE、短寿命トークン、適切なスコープを要求しているからです。

5. ChatGPT App の世界で OAuth 2.1 + PKCE を前提に考える理由

歴史的文脈を踏まえた上で、ChatGPT と MCP の視点から見てみましょう。

パブリッククライアントとしての ChatGPT

ChatGPT(および MCP Jam のようなクライアント)は、あなたの認可サーバーに対して典型的な public client です。

  • client_secret を信頼して保持することができません。
  • OpenAI のインフラで動作し、あなたはそれをコントロールできません。

したがって、唯一まともな選択は Authorization Code Flow + PKCE です。安全性はクライアントシークレットではなく、コードチャレンジとコードベリファイアの検証に基づきます。

Apps SDK の公式ドキュメントは、MCP クライアントとして動作する ChatGPT が Authorization Code + PKCE(S256)フローを実行し、あなたの認可サーバーがメタデータで PKCE をサポートしていない場合は認可を完了しないと明記しています: code_challenge_methods_supported: ["S256"]

MCP の観点でのフローの見え方

かなり大まかですが、有用なイメージとしては(保護されたリソースのためのシーケンス)次の通りです。

sequenceDiagram
    participant U as ユーザー
    participant C as ChatGPT (MCP Client)
    participant AS as Auth Server
    participant RS as MCP Server (Resource)

    U->>C: "自分の注文を見せて"
    C->>RS: call_tool(list_orders)(トークンなし)
    RS-->>C: エラー + _meta["mcp/www_authenticate"]
    C->>AS: ログイン/同意画面を開く(Authorization Code + PKCE)
    U->>AS: ログインして同意する(scopes)
    AS-->>C: Authorization Code
    C->>AS: コードを Access Token に交換(+PKCE 検証)
    AS-->>C: Access Token (Bearer)
    C->>RS: call_tool(list_orders)(Authorization: Bearer <token>)
    RS->>RS: 署名, issuer, audience, scopes を検証
    RS-->>C: ユーザーの注文一覧
    C-->>U: データを表示

サーバー側では、次のものを利用します。

  • 保護リソースのメタデータ(/.well-known/oauth-protected-resource)— 自身をリソースとして宣言し、そのリソースを扱う認可サーバーを示します。
  • ヘッダー Authorization: Bearer <token> に載ってくるトークンを、 JWK による JWT 検証、または認可サーバーでイントロスペクションします。
  • トークンの audience や scopes が不適切ならリクエストを拒否し、 再度 WWW-Authenticate チャレンジを _meta["mcp/www_authenticate"] に載せて返し、 ChatGPT に必要なパラメータで認可をやり直させます。

あなたのコードから見ると、とても人間的です。検証済みの AuthContext を受け取り、それを使って処理するだけです。

ミニ例: MCP ツールが匿名ユーザーと認証済みユーザーをどう区別するか

まだ具体的な OAuth SDK なしの概念例です。

import type { McpToolHandler } from "./types";

export const listOrders: McpToolHandler = async (_args, context) => {
  const auth = context.auth; // ここにトークン検証の結果を入れているとする

  if (!auth) {
    return {
      content: [{ type: "text", text: "注文を見るにはログインが必要です。" }],
      _meta: {
        // ChatGPT へのチャレンジ: OAuth フローを開始して
        "mcp/www_authenticate": [
          'Bearer resource_metadata="https://mcp.giftgenius.app/.well-known/oauth-protected-resource", error="insufficient_scope", error_description="Login required to view orders"'
        ]
      },
      isError: true
    };
  }

  const orders = await db.orders.findMany({ where: { userId: auth.userId } });

  return {
    content: [{ type: "text", text: `見つかった注文数: ${orders.length}` }],
    structuredContent: orders
  };
};

まさにこの _meta["mcp/www_authenticate"] ヒントが、ChatGPT 側の OAuth UI を起動するトリガーとして、Apps SDK の公式ドキュメントに記載されています。

6. 「短寿命トークンと最小限のスコープ」とは実務で何を意味するか

仕様やガイドから、次の講義(具体的な IdP 設定)に入る前から意識すべき重要な原則がいくつか導かれます。

トークンの有効期間を短く

Access token は短命であるべきです。なぜか?

  • 漏えいしても、攻撃者が使える時間が限られます。
  • ユーザーの権限を安全に変更でき、短時間でトークンが失効して新しいものが取得されます。

通常は数分〜数十分です。その代わりに refresh token や再認可が必要になりますが、ChatGPT の文脈では多くの面倒をクライアント側が肩代わりしてくれます。

スコープで権限を絞る

スコープは gifts.readgifts.writeorders.readorders.checkout のような文字列です。これは、このリソースの範囲でユーザーが何に対して権利を持つかを示します。

ChatGPT App では特に重要です。

  • ウィッシュリストの閲覧だけなら、gifts.read だけを持つトークンを発行できます。
  • ACP/Instant Checkout のような操作では、orders.checkout のように、より強い権限を明示的に要求し、ユーザーにもはっきり提示するのが妥当です。

MCP のツール定義では、特定ツールに必要な scopes を持つ securitySchemes を宣言できます。これにより、ChatGPT はそのツール呼び出しにどの権限が必要かを把握できます。

Audience: トークンは当該 MCP リソース向けのもの

もう 1 つ重要なのが aud(audience)です。MCP サーバーは、トークンが本当に 自分向けに発行されたものか、別のサービスのものではないかを確認すべきです。

Apps SDK のドキュメントには、ChatGPT が resource パラメータを渡し、認可サーバーがそれを(通常は aud に)反映し、MCP サーバーはそのフィールドを検証することが期待されると、はっきり書かれています。

アプリ審査では、偽の auth_token を渡してあなたの実装に穴がないか確かめられる可能性が高いです。最初から正しく実装しておきましょう。

7. GiftGenius に当てはめると

学習用 App に話を戻します。現状はおおむね次のような状況です。

  • 受け手の説明と予算からギフトアイデアを提案する MCP ツール get_gift_ideas。これは匿名でも動きます。
  • リストを DB に保存する MCP ツール save_gift_list。特定のユーザーにひも付けたいです。
  • ユーザーが保存したすべてのリストを表示する MCP ツール list_saved_lists。これは確実に認証が必要です。

ウィジェットは美しいギフトカードを表示し、「保存」や「購入済みとしてマーク」をクリックできるようにします — これは実質、保護された MCP ツールのフロントです。

型レベルでは次のようになります。

// ツール呼び出しコンテキストの型(簡略版)
interface ToolContext {
  auth: AuthContext | null;
}

// 保護されたツールの例
async function listSavedGiftLists(_input: {}, context: ToolContext) {
  if (!context.auth) {
    // 上と同じく mcp/www_authenticate のトリックを使う
    throw new Error("Authentication required");
  }

  return db.giftLists.findMany({
    where: { ownerId: context.auth.userId }
  });
}

このような関数を書いた瞬間に明らかになります。単なる「.env の API キー」では何の助けにもなりません。検証済みの OAuth トークンに基づく、きちんとした AuthContext が必要です。

どの部分が匿名で動き、どれが要認証か

OAuth を設定する前の良い練習は、機能を正直に 2 つに仕分けることです。

GiftGenius の例:

匿名:

  • 説明に基づくギフトアイデアの生成。
  • サンプルの表示とダミーデータのデモモード。

認証済みのみ:

  • 個人のウィッシュリストの閲覧と編集。
  • 注文履歴。
  • あらゆる支払い操作、Instant Checkout、ACP 連携。

次の講義では、Auth Server(たとえば Keycloak や Hydra+Kratos)と MCP サーバーを設定し、これらの操作に必要なスコープを持つトークンを発行し、MCP ツールが適切に拒否し、ChatGPT に再認可を促せるようにしていきます。

8. ChatGPT App の認証でありがちな誤解

誤り1: 「ChatGPT がすでにユーザーを知っているのだから、独自ログインは不要」
多くの人が「ChatGPT にはユーザーアカウントがある。userId としてそれを使えばいいのでは?」と考えます。しかし ChatGPT はあなたにユーザーの実アイデンティティを開示しませんし、自身のアカウントへのアクセスも与えません。MCP メタデータで見えるのはせいぜい匿名の _meta["openai/subject"] で、これはレート制限とセッション識別のためのものであり、認可や実アカウントへのひも付けには使ってはならないと明記されています。

誤り2: 「みんなで 1 本の API キーで十分。所詮『統合』でしょ」
MCP サーバーに backend 用の API キーを埋め込んで満足する方式は、ChatGPT の全ユーザーがあなたのサービスの 1 つのアカウントを共有するシナリオでしか機能しません。個人データやコマース、ACL が出てきた瞬間に、ユーザーを区別して権限を管理することができなくなります。API キーはサービスのアイデンティティであって、ユーザーのものではありません。

誤り3: 「password grant を使えば最短でできる」
ユーザーのログイン/パスワードを backend に渡してトークンに交換する(Resource Owner Password Credentials Grant)習慣は、OAuth 2.0 初期の古くて安全でないパターンです。現代の推奨事項や OAuth 2.1 の文脈ではアンチパターンとされます。ChatGPT のような public クライアントは、あなたのユーザーのパスワードを見るべきではありません — そのために Authorization Code + PKCE が存在します。

誤り4: 「PKCE は余計な複雑さ。抜いてしまおう」
PKCE(特に S256)は、public クライアント向け Authorization Code Flow を守るための必須メカニズムです。PKCE がなければ、盗まれた authorization code を再利用され得ます。MCP Authorization の仕様と Apps SDK には、認可サーバーのメタデータで PKCE サポートの宣言が必要で、ChatGPT はまさにこの仕組みを使うと明記されています。これを無効化すると、フローは動きません。

誤り5: 「念のため、可能な scopes を全部要求しておこう」
「ディスク C: までフォーマットできる」ようなトークンを作りたくなることがありますが、これは最小権限の原則(PoLP)に反し、OpenAI と多くの IdP のポリシーにも反します。ChatGPT App に本当に必要な scopes をきちんと考えましょう。閲覧用、書き込み用、コマース用などに分けるのが良いです。これはセキュリティを高めるだけでなく、同意の UX にも効きます。ユーザーが、意味不明な 20 行の権限ではなく、分かりやすく限定された権限セットを見ることになります。

誤り6: 「MCP サーバーがログイン/パスワードを保管してログイン UI も描けばいい」
MCP サーバーは Resource Server であり、Auth Server ではありません。トークンを検証し、自身の .well-known メタデータを公開し、WWW-Authenticate チャレンジを返せれば十分で、ログイン処理やパスワードの保管を担うべきではありません。ログイン/同意は専門の認可サーバー(Keycloak、Hydra、Auth0 など)を使うのがベストです。これは次の講義で実際に見ていきます。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION