AIエージェント入門

MCP認可の実装ガイド|OAuth 2.1対応

MCP認可の実装ガイド OAuth 2.1対応 サムネイル

この記事の結論

MCP認可をOAuth 2.1、Resource Indicators、PKCEで安全に実装する手順を、FastAPIコード例と設定チェックリストで解説します。

「MCPサーバーを社内ツールにつないだ瞬間、認可まわりが急に難しくなった」——最近のAIエージェント実装で、ここに詰まるケースが増えています。

ローカルで動くMCPなら簡単です。ところが、SaaS API、社内CRM、ナレッジベース、請求システムなどに接続し始めると、「誰の権限で」「どのサーバー向けに」「どの範囲だけ」ツールを呼べるのかを明確にしないと危険です。

この記事では、Model Context Protocol(MCP)の最新仕様である 2025-11-25版 の認可仕様をもとに、OAuth 2.1、PKCE、Resource Indicators、Protected Resource Metadataをどう組み合わせるかを、実装担当者向けに整理します。

MCP認可で最初に決めるべき3つの境界

MCP認可の設計で最初に見るべきなのは、ログイン画面ではありません。境界です。どのMCPサーバーが保護リソースで、どの認可サーバーがトークンを発行し、どのクライアントがそのトークンを使うのかを先に決めます。

境界 決めること 実装上の注意
MCPサーバー どのツール・リソースを公開するか canonical URIを固定し、トークンの対象にする
認可サーバー 誰に、どのscopeを許可するか OAuth 2.1相当のフロー、PKCE、redirect URI検証を使う
MCPクライアント どのサーバー向けのトークンを要求するか authorization request と token request の両方に resource を入れる

公式仕様では、保護されたMCPサーバーはOAuth 2.1のresource serverとして振る舞います。つまり、MCPサーバー自身がユーザー認証を全部抱え込むのではなく、アクセストークンを受け取り、そのトークンが自分宛てかどうかを検証する立ち位置です。

resourceパラメータを必ず入れる

最新仕様で特に重要なのが、RFC 8707の Resource Indicators です。MCP仕様では、authorization request と token request の両方に resource パラメータを含めることが求められています。これにより、トークンの宛先が「どのMCPサーバーなのか」を明示できます。

まずは、認可URLを作る関数を小さく切り出すのが安全です。

# 動作環境: Python 3.11+
# 注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。
from urllib.parse import urlencode
AUTHORIZATION_ENDPOINT = "https://auth.example.com/oauth2/authorize"
CLIENT_ID = "https://client.example.com/oauth/client-metadata.json"
REDIRECT_URI = "https://client.example.com/oauth/callback"
MCP_RESOURCE = "https://mcp.example.com"
def build_authorization_url(state: str, code_challenge: str) -> str:
    params = {
        "response_type": "code",
        "client_id": CLIENT_ID,
        "redirect_uri": REDIRECT_URI,
        "scope": "mcp:tools.read mcp:tools.call",
        "state": state,
        "code_challenge": code_challenge,
        "code_challenge_method": "S256",
        "resource": MCP_RESOURCE,
    }
    return f"{AUTHORIZATION_ENDPOINT}?{urlencode(params)}"

ポイントは、scopeだけで安心しないことです。scopeは「何ができるか」、resourceは「どのリソース向けか」を表します。複数のMCPサーバーを使うエージェントでは、この区別がないとトークンの使い回しやconfused deputy問題につながります。

トークン交換時もresourceを落とさない

authorization request にresourceを入れても、token requestで落とすと意味が薄れます。MCP仕様では両方に含める前提なので、トークン交換の関数にも必ずresourceを渡します。

# 動作環境: Python 3.11+, httpx>=0.27
# 注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。
import httpx
TOKEN_ENDPOINT = "https://auth.example.com/oauth2/token"
MCP_RESOURCE = "https://mcp.example.com"
async def exchange_code_for_token(code: str, code_verifier: str) -> dict:
    data = {
        "grant_type": "authorization_code",
        "code": code,
        "redirect_uri": "https://client.example.com/oauth/callback",
        "client_id": "https://client.example.com/oauth/client-metadata.json",
        "code_verifier": code_verifier,
        "resource": MCP_RESOURCE,
    }
    async with httpx.AsyncClient(timeout=10.0) as client:
        res = await client.post(TOKEN_ENDPOINT, data=data)
        res.raise_for_status()
        return res.json()

ここでclient secretをコードに直書きしないことも大事です。confidential clientでsecretを使う場合は、環境変数やシークレットマネージャーに置き、ログにも出さないようにします。

MCPサーバー側ではaudienceとissuerを検証する

アクセストークンを受け取ったMCPサーバー側では、「署名が正しい」だけでは不十分です。少なくとも issuer、audience、scope、期限を確認します。JWTを使う場合の最小構成は次のようになります。

# 動作環境: Python 3.11+, fastapi>=0.110, pyjwt[crypto]>=2.8
# 注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。
from fastapi import Depends, FastAPI, HTTPException, Request
import jwt
app = FastAPI()
ISSUER = "https://auth.example.com"
AUDIENCE = "https://mcp.example.com"
JWKS_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----n...n-----END PUBLIC KEY-----"
async def require_mcp_token(request: Request) -> dict:
    auth = request.headers.get("authorization", "")
    if not auth.lower().startswith("bearer "):
        raise HTTPException(status_code=401, detail="missing bearer token")
    token = auth.split(" ", 1)[1]
    try:
        claims = jwt.decode(token, JWKS_PUBLIC_KEY, algorithms=["RS256"], issuer=ISSUER, audience=AUDIENCE)
    except jwt.PyJWTError:
        raise HTTPException(status_code=401, detail="invalid token")
    scopes = set(claims.get("scope", "").split())
    if "mcp:tools.call" not in scopes:
        raise HTTPException(status_code=403, detail="insufficient scope")
    return claims
@app.post("/mcp/tools/call")
async def call_tool(claims: dict = Depends(require_mcp_token)):
    return {"ok": True, "subject": claims.get("sub")}

この例では audience="https://mcp.example.com" を固定しています。別のMCPサーバー向けに発行されたトークンを受け付けないための、かなり重要な行です。

Protected Resource Metadataを返す

クライアントがどの認可サーバーに行けばよいか分からない場合、Protected Resource Metadataが役立ちます。RFC 9728では、保護リソースがメタデータURLを示す仕組みが定義されています。MCPでもこの考え方が採用されています。

# 動作環境: Python 3.11+, fastapi>=0.110
# 注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。
from fastapi import FastAPI, Response
app = FastAPI()
RESOURCE = "https://mcp.example.com"
AUTH_SERVER = "https://auth.example.com"
@app.get("/.well-known/oauth-protected-resource")
async def protected_resource_metadata():
    return {"resource": RESOURCE, "authorization_servers": [AUTH_SERVER], "scopes_supported": ["mcp:tools.read", "mcp:tools.call"], "bearer_methods_supported": ["header"]}
@app.get("/mcp")
async def mcp_entrypoint():
    return Response(status_code=401, headers={"WWW-Authenticate": 'Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"'})

本番では、メタデータのURL、resource値、実際のMCPサーバーURLが食い違わないようにします。リバースプロキシやカスタムドメインを使っている場合、ここがズレやすいです。

よくある失敗パターンと回避策

失敗1:scopeだけで認可を完結させる
scopeは便利ですが、トークンの宛先までは表しません。MCPサーバーが複数あるならresource/audience検証まで入れてください。

失敗2:PKCEを省略する
ブラウザやデスクトップアプリを含むpublic clientでは、認可コードの横取り対策が重要です。MCP仕様でもPKCEの利用が前提になっています。

失敗3:トークンをそのまま下流SaaSへ渡す
token passthroughは、権限の境界を曖昧にします。MCPサーバー側で必要最小限の権限に変換するか、ツールごとにscopeを分ける設計にしてください。

失敗4:ローカル開発用の例外が本番に残る
localhost、自己署名証明書、広すぎるCORS、private IP許可などは開発では便利ですが、本番では攻撃面になります。設定を環境ごとに分けるのが安全です。

実装チェックリスト

  • MCPサーバーのcanonical URIを1つに決めたか
  • authorization request と token request の両方にresourceを入れたか
  • PKCEのS256方式を使っているか
  • MCPサーバー側でissuer、audience、scope、expを検証しているか
  • Protected Resource Metadataを返せるか
  • ツールごとに必要scopeを分けたか
  • ログにaccess token、authorization code、client secretを出していないか
  • 開発環境の例外設定が本番に残っていないか

すでにMCPサーバーの作り方を確認したい方は、MCPの基礎解説と、PythonでMCPサーバーを作るガイドもあわせて読むと全体像をつかみやすいです。エージェント全体のガードレール設計は、AIエージェントのガードレール解説も参考になります。

まとめ:今日から始める3つのアクション

  1. 今日やること:MCPサーバーごとのcanonical URIを一覧化し、resource値として固定する
  2. 今週中:認可URLとトークン交換処理にresourceとPKCEを入れ、ステージングで検証する
  3. 今月中:audience検証、scope分割、Protected Resource Metadataを本番構成に組み込む

参考・出典

この記事を読んで導入イメージが固まってきた方へ

UravationではAIエージェント導入の研修・コンサルを行っています。

著者: 佐藤傑(さとう・すぐる)。株式会社Uravation代表取締役。X(@SuguruKun_ai)フォロワー約10万人。著書『AIエージェント仕事術』。

Need help moving from reading to rollout?

この記事を読んで導入イメージが固まってきた方へ

Uravationでは、AIエージェントの要件整理、PoC設計、社内導入、研修まで一気通貫で支援しています。

この記事をシェア

X Facebook LINE

※ 本記事の情報は2026年5月時点のものです。サービスの料金・仕様は変更される可能性があります。最新情報は各サービスの公式サイトをご確認ください。

関連記事