「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つのアクション
- 今日やること:MCPサーバーごとのcanonical URIを一覧化し、resource値として固定する
- 今週中:認可URLとトークン交換処理にresourceとPKCEを入れ、ステージングで検証する
- 今月中:audience検証、scope分割、Protected Resource Metadataを本番構成に組み込む
参考・出典
- Authorization – Model Context Protocol — MCP公式仕様 2025-11-25版(参照日: 2026-05-08)
- Security Best Practices – Model Context Protocol — MCP公式セキュリティ指針(参照日: 2026-05-08)
- RFC 8707: Resource Indicators for OAuth 2.0 — IETF RFC(参照日: 2026-05-08)
- RFC 9728: OAuth 2.0 Protected Resource Metadata — IETF RFC(参照日: 2026-05-08)
- OAuth 2.1 Authorization Framework draft — IETF Datatracker(参照日: 2026-05-08)
この記事を読んで導入イメージが固まってきた方へ
UravationではAIエージェント導入の研修・コンサルを行っています。
著者: 佐藤傑(さとう・すぐる)。株式会社Uravation代表取締役。X(@SuguruKun_ai)フォロワー約10万人。著書『AIエージェント仕事術』。
