MCPサーバーにOAuth認可を実装する7ステップ:PRM・トークン検証・スコープ設計【MCPセキュリティ/2026-07-28版】
2026-07-28版仕様に沿って、リモートMCPサーバーにOAuth認可を実装する手順を7ステップで解説。正規URI、認可サーバーの選び方、Protected Resource Metadata、401チャレンジ、宛先を確認するトークン検証、ツール単位のスコープと403ステップアップ、テスト済みPythonコード、curlでの5つの確認をまとめます。
最終更新:2026年10月16日 / 対象:MCP仕様 2026-07-28版(Authorization、Authorization Server Discovery、Client Registration、Authorization Security Considerations)、RFC 9728、RFC 8707、RFC 9068
結論(30秒で読める要約)
MCPのOAuth認可とは、MCP仕様がHTTP系トランスポート向けに定めている、OAuth 2.1ベースの認可の仕組みです。リモートMCPサーバーは「リソースサーバー」、トークンを発行するのは別の(または同居の)「認可サーバー」、ブラウザでのログインはクライアントの担当です。MCPセキュリティの観点で、サーバー側の作業は次の7ステップに集約できます。
- サーバーの正規URI(例:
https://mcp.example.com/mcp)を1つに決める。これがトークンの宛先(audience)になる。 - PKCE(
S256)とresourceパラメーターに対応し、できればClient ID Metadata Documentsにも対応した認可サーバーを選ぶ。 - Protected Resource Metadata(RFC 9728)を公開する。仕様上のMUST。
- トークンなしのリクエストには
401と、そのメタデータを指すWWW-Authenticateヘッダーを返す。 - 毎回トークンを検証する。署名・発行者・宛先・有効期限・トークン種別。不正や期限切れは
401。 - スコープをツールに対応づけ、足りないときは不足分をすべて1回で示した
403 insufficient_scopeを返す。 - 下流のAPIはサーバー自身の資格情報で呼ぶ。クライアントのトークンは転送しない。
5章にステップ3〜6を実装したPythonのコード(テスト済み)、6章に稼働中のサーバーを curl で5分で確認する手順を載せています。
目次
- MCPのOAuth認可とは
- 誰が何を実装するのか
- 2026-07-28版でサーバー実装者に関係する変更
- 実装の7ステップ
- 実装例(Python・テスト済み)
- curlで自分のサーバーを確認する
- ステータスコードとヘッダーの早見表
- 実装チェックリスト(15項目)
- よくあるエラー(症状→原因→対処)
- NodeFlareならこう解決できる
- よくある質問(FAQ)
1. MCPのOAuth認可とは
MCPのOAuth認可とは、クライアントが「そのMCPサーバー専用に発行されたOAuth 2.1アクセストークン」を使って、保護されたMCPサーバーをHTTP経由で呼び出すための仕組みです。OAuth 2.1ドラフトの一部に、RFC 6750(Bearerトークン)、RFC 8414(認可サーバーメタデータ)、RFC 8707(リソースインジケーター)、RFC 9728(保護リソースメタデータ)、RFC 9207(発行者の識別)、OAuth Client ID Metadata Documentsを組み合わせています。
前提として押さえておきたいのは次の3点です。
- MCPでは認可は必須ではない(OPTIONAL)。ただし、HTTP系のサーバーが認可に対応するなら、この仕様に従うべき(SHOULD)とされています。stdioのサーバーは従うべきではなく(SHOULD NOT)、資格情報は環境変数などから取得します。
- サーバーはリソースサーバーであって、ログイン画面ではない。認可サーバーの中身は仕様の対象外です。サーバー側の仕事は「トークンをどこで取るか」を知らせ、受け取ったトークンを厳密に検証することです。
- 実際のトラブルの多くはサーバー側で起きる。メタデータがない、
401に必要なヘッダーがない、宛先を確認せずにトークンを受け入れている、の3つが典型です。
MCPセキュリティ全体の脅威モデルを先に把握したい場合は、MCPセキュリティのベストプラクティスとチェックリストを読んでください。この記事はその中の1領域、「自分のサーバーに認可を実装する」部分を掘り下げます。
2. 誰が何を実装するのか
MCP仕様は、認可の流れを3つの役割に分けています。どのMUSTが誰の担当かを把握しておくと、デバッグがかなり楽になります。
| 役割 | 実体 | 2026-07-28版での主な義務 |
|---|---|---|
| MCPサーバー(リソースサーバー) | あなたのリモートMCPエンドポイント | RFC 9728メタデータの実装(MUST)、トークンと宛先の検証(MUST)、不正・期限切れトークンには 401、他のトークンの受け入れや転送の禁止(MUST NOT) |
| 認可サーバー | 社内のIdPやOAuthサーバー | OAuth 2.1の実装(MUST)、RFC 8414またはOpenID Connect Discoveryの提供(MUST)、Client ID Metadata Documentsへの対応(SHOULD)、短命なアクセストークン(SHOULD)、公開クライアントのリフレッシュトークンのローテーション(MUST) |
| MCPクライアント | Claude、IDE、エージェント | 可能な限りPKCE S256(MUST)、認可・トークン要求での resource 送信(MUST)、コードを使う前のRFC 9207 iss 検証(MUST)、毎リクエストで Authorization ヘッダーにトークン(MUST) |
直接コントロールできるのは1行目だけです。ただ、認可サーバーを選ぶのはあなたなので、2行目も実質的にはあなたの責任です。
3. 2026-07-28版でサーバー実装者に関係する変更
2025年に書かれた解説の多くは、Dynamic Client Registration(DCR)を前提にしています。現在の仕様ではそうではありません。
| 項目 | 以前(2025年版) | 2026-07-28版 | 実装者への影響 |
|---|---|---|---|
| クライアント登録 | DCR(RFC 7591)が広く推奨 | Client ID Metadata Documentsに対応すべき(SHOULD)。DCRは非推奨で、後方互換のために残る | client_id_metadata_document_supported: true を広告する認可サーバーを選ぶ |
| mix-up攻撃対策 | 必須ではない | クライアントは iss 検証(RFC 9207)がMUST。iss を返す認可サーバーは authorization_response_iss_parameter_supported の広告がMUST |
iss を返す認可サーバーを選ぶ。将来MUSTに格上げされる見込みと明記 |
| スコープの要求 | ステップアップの記述あり | その操作に必要なスコープは1回のチャレンジにまとめるべき(SHOULD)。クライアントは既存と新規の和集合を要求 | ツールごとに必要なスコープ全体を計算して返す |
| スコープの階層 | 記述なし | 上位スコープが下位を含む階層を考慮すること(MUST) | 設計上 invoices:write が invoices:read を含むなら、判定もそれに従う |
offline_access |
記述なし | scopes_supported やチャレンジに含めない(SHOULD NOT) |
リフレッシュトークンはクライアントと認可サーバーに任せる |
4. 実装の7ステップ
全体像は下の表のとおりです。続く小見出しで1つずつ説明します。
| ステップ | 成果物 | 仕様上の根拠 |
|---|---|---|
| 1. 正規URI | どこでも同じURI文字列 | RFC 8707 §2、MCP「Canonical Server URI」 |
| 2. 認可サーバー | 下の要件表を満たすもの | MCP Authorization概要、Security Considerations |
| 3. Protected Resource Metadata | well-known URLのJSON | RFC 9728、MCP Authorization Server Discovery |
| 4. 401チャレンジ | resource_metadata と scope を含む WWW-Authenticate |
RFC 9728 §5.1、RFC 6750 §3 |
| 5. トークン検証 | 自分宛てのトークンだけを受け入れる | OAuth 2.1 §5.2、RFC 8707、RFC 9068 §4 |
| 6. ツール単位のスコープとステップアップ | 不足分をまとめた 403 insufficient_scope |
MCP「Scope Challenge Handling」 |
| 7. 下流の呼び出し | 上流APIごとの自前の資格情報 | MCP「Access Token Privilege Restriction」 |
ステップ1:正規URIを1つに決める
正規URIは、OAuthのリソースとしてMCPサーバーを識別する文字列です。メタデータ、クライアントが送る resource パラメーター、トークンの aud クレームのすべてで同じ文字列を使います。仕様が挙げる有効な例は https://mcp.example.com/mcp、https://mcp.example.com、https://mcp.example.com:8443。スキームがないものや #fragment を含むものは無効です。
実務上のルールは2つです。
- 末尾のスラッシュを付けない。仕様は、意味がある場合を除いてスラッシュなしの形を使うべき(SHOULD)としています。5章のテストでも、
audが同じURIに末尾スラッシュを付けただけのトークンはPyJWTで拒否されました。これは正しい厳密な動作で、「トークンは正しいはずなのに401になる」原因としてよく見かけます。 - 1つのホストで複数のMCPサーバーを動かすならパスまで含める(例:
https://mcp.example.com/server/mcp)。そうしないと、隣のサーバー向けのトークンが通ってしまいます。
ステップ2:MCPに対応できる認可サーバーを選ぶ
認可サーバーを自作することはまずありません。コードを書く前に、使っているIdPやOAuth製品が次の要件を満たすか確認してください。
| 要件 | 確認場所 | 理由 |
|---|---|---|
| RFC 8414またはOpenID Connect Discoveryのメタデータ | /.well-known/oauth-authorization-server または /.well-known/openid-configuration |
クライアントが必ず探しに来る。文書内の issuer はURLを組み立てた発行者と完全一致が必要(MUST) |
code_challenge_methods_supported に S256 がある |
認可サーバーのメタデータ | この項目がないと、MCPクライアントは処理を中止しなければならない(MUST) |
resource パラメーターを受け取り aud に入れる |
テストログイン後のトークンの中身 | これがないと宛先の検証ができない |
| Client ID Metadata Documents | client_id_metadata_document_supported: true |
2026-07-28版で推奨される、初対面のクライアントの登録方法 |
認可レスポンスで iss を返す |
authorization_response_iss_parameter_supported: true |
RFC 9207のmix-up攻撃対策が有効になる |
| 短命なアクセストークンとリフレッシュトークンのローテーション | 認可サーバーの設定 | 仕様でSHOULD、公開クライアントではMUST |
| すべてHTTPS、リダイレクトURIの完全一致 | 認可サーバーの設定 | Security ConsiderationsでMUST |
認可サーバーが resource の値を aud に入れられない場合は、そのサーバーが扱える専用のaudience値を使っても構いません。その場合も、値はこのMCPサーバー固有にし、同じように検証します。
ステップ3:Protected Resource Metadataを公開する
Protected Resource Metadata(RFC 9728)は、「このサーバー向けのトークンはどの認可サーバーが発行するか」をクライアントに伝える小さなJSONです。仕様では、MCPサーバーはこれを実装しなければならず(MUST)、認可サーバーを少なくとも1つ記載しなければなりません(MUST)。
{
"resource": "https://mcp.example.com/mcp",
"authorization_servers": ["https://auth.example.com"],
"scopes_supported": ["mcp:basic"],
"bearer_methods_supported": ["header"],
"resource_name": "Example Invoices MCP"
}
置き場所は次の2つで、ヘッダーで場所が示されていない場合、クライアントはこの順に探します。
- パス付き:
https://mcp.example.com/.well-known/oauth-protected-resource/mcp - ルート:
https://mcp.example.com/.well-known/oauth-protected-resource
注意点は次のとおりです。
resourceは、クライアントが使った識別子と完全に一致していなければなりません(RFC 9728 §3.3)。一致しない場合、クライアントはその内容を使ってはいけません(MUST NOT)。ステップ1の正規URIと同じにします。scopes_supportedは基本機能に必要な最小限にします。持っているスコープを全部並べるのではなく、権限の強いスコープは後からステップアップで要求させます。offline_accessは載せません。bearer_methods_supported: ["header"]で、クエリ文字列のトークンを受け付けないことを明示します(そもそもMCP仕様で禁止されています)。
ステップ4:正しい401チャレンジを返す
新しいクライアントからの最初のリクエストにはトークンがありません。サーバーは 401 と WWW-Authenticate ヘッダーを返し、必要なスコープも含めるべき(SHOULD)とされています。
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp",
scope="mcp:basic"
クライアントはまずこのチャレンジの scope を使い、ない場合にだけ scopes_supported を使います。つまり、ログイン時の最小権限をコントロールするのはメタデータではなくこのチャレンジです。クライアントは、チャレンジで示されたスコープをその操作に必要なものとして扱わなければなりません(MUST)。
initialize や tools/list を含め、MCPエンドポイントへの認証なしのHTTPリクエストにはすべてこれを返します。tools/list を匿名で通すと、どんなツールがあるかが外から見えてしまいます。
ステップ5:毎回アクセストークンを検証する
宛先(audience)の拘束が実際に効くのは、このトークン検証の段階です。サーバーは自分宛てに発行されたトークンかを確認し、それ以外は 401 で拒否し、他サービス向けのトークンを受け入れたり転送したりしてはいけません。いずれもMUSTです。
JWT形式のアクセストークンなら、RFC 9068 §4に具体的な手順があります。
| 確認項目 | ルール | よくある不具合 |
|---|---|---|
typ ヘッダー |
at+jwt または application/at+jwt |
IDトークン(typ: JWT)をアクセストークンとして受け入れる |
署名と alg |
認可サーバーの鍵で検証。alg: none は拒否 |
許可するアルゴリズムを固定していない |
iss |
想定する発行者と完全一致 | トークンが名乗る発行者をそのまま信じる |
aud |
自分の正規URIを含む | 検証していない、または末尾スラッシュ付きで比較している |
exp |
現在時刻より後。わずかな猶予は可 | 猶予ゼロで時計のずれにより失敗する |
| 必須クレーム | iss、exp、aud、sub、client_id、iat、jti |
欠けているクレームを有効扱いする |
不透明(opaque)トークンの場合は、認可サーバーのイントロスペクション(RFC 7662)を呼び、active: true を確認したうえで、応答に対して同じ宛先とスコープの確認をします。成功結果のキャッシュは短時間にとどめ、失効が効くようにします。
公式SDKを使うと楽になりますが、何をしてくれるのかは確認しておきましょう。
- MCP TypeScript SDK v2のドキュメントでは、宛先を確認する
expectedResourceオプション付きのrequireBearerAuthと、パス付きの場所にメタデータを配信するmcpAuthMetadataRouterが紹介されています。宛先の確認はexpectedResourceを設定したときだけ行われること、@modelcontextprotocol/express2.0.1はこのオプションを渡していなかったため@modelcontextprotocol/serverと一緒に更新すること、も書かれています。 - MCP Python SDKには
TokenVerifierインターフェースとAuthSettings(issuer_url、resource_server_url、required_scopes)があります。検証処理の中身は自分で書くので、宛先の確認も自分の責任です。
ステップ6:スコープをツールに対応づけ、403でステップアップする
スコープは「有効なトークンで何ができるか」を決めます。MCPセキュリティとしては、リスクが同じ程度の少数のツールに1つのスコープを対応させ、ツールを実行する前にサーバー側で強制するのが効果的です。
| 段階 | ツールの例 | スコープ | 付与のタイミング |
|---|---|---|---|
| 基本 | initialize、tools/list |
mcp:basic |
初回ログイン時(401チャレンジから) |
| 読み取り | get_invoice、search_customers |
invoices:read |
初めて使うときにステップアップ |
| 書き込み | refund_invoice |
invoices:write |
初めて使うときにステップアップ+クライアント側で人の確認 |
有効なトークンでもスコープが足りなければ、その操作に必要なスコープをすべて含めた 403 を1回で返します。
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
scope="invoices:write mcp:basic",
resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp",
error_description="Refunds require invoices:write"
クライアントは、それまでのスコープとチャレンジのスコープの和集合で再認可します。サーバー側のルールは3つです。
- 未知のツールは拒否する。スコープの対応表にないツールは、基本スコープで通すのではなく失敗させます(fail closed)。
- スコープの階層を扱う。
invoices:writeがinvoices:readを含む設計なら、判定もそれに合わせます。仕様上のMUSTです。 - できれば
tools/listをスコープで絞る。仕様はツール一覧が認可内容によって変わることを認めているので、呼べるツールだけを見せられます。人間以外の呼び出し元に狭いスコープの資格情報を発行する方法は、MCPのAPIキー管理プレイブックにまとめています。
ステップ7:下流APIは自前の資格情報で呼ぶ
MCPサーバーがSaaSのAPIを包んでいる場合、そのAPIは受け取ったトークンではなく、サーバー自身の資格情報で呼びます。仕様は、クライアントから受け取ったトークンの転送(token passthrough)を禁止しています(MUST NOT)。上流で使うのは、上流の認可サーバーが発行した別のトークンです。
その資格情報は暗号化したシークレットとしてサーバー側に保管し、上流で必要な範囲にスコープを絞り、クライアント向けのトークンとは別にローテーションします。また、あなたのサーバー自体が固定のクライアントIDで第三者の認可サーバーへつなぐOAuthプロキシである場合は、動的に登録されたクライアントごとに、転送前にユーザーの同意を得ることがMUSTです。
5. 実装例(Python・テスト済み)
次のモジュールは、特定のWebフレームワークに依存せずにステップ3〜6を実装したものです。メタデータ、401と403のチャレンジ、JWTの厳密な検証、ツール単位のスコープを含みます。HTTPミドルウェアから verify_access_token と authorize を呼び、失敗したら AuthError.status と WWW-Authenticate ヘッダーを返してください。Python 3.13、PyJWT 2.14で動作を確認しています。
"""Resource-server checks for a remote MCP server (MCP spec 2026-07-28).
Requires PyJWT>=2.8 with cryptography. Tested with PyJWT 2.14, Python 3.13."""
import jwt
from jwt import PyJWKClient
RESOURCE = "https://mcp.example.com/mcp" # canonical URI, no trailing slash
ISSUER = "https://auth.example.com" # must match AS metadata "issuer" exactly
PRM_URL = "https://mcp.example.com/.well-known/oauth-protected-resource/mcp"
ALLOWED_ALGS = ["RS256", "ES256"] # never "none", never HS* with a public key
BASE_SCOPES = {"mcp:basic"} # enough for initialize / tools/list
TOOL_SCOPES = { # one entry per tool; unknown tools are denied
"get_invoice": {"invoices:read"},
"refund_invoice": {"invoices:write"},
}
PROTECTED_RESOURCE_METADATA = { # served at PRM_URL (RFC 9728)
"resource": RESOURCE,
"authorization_servers": [ISSUER],
"scopes_supported": sorted(BASE_SCOPES), # minimal set; the rest via step-up
"bearer_methods_supported": ["header"],
"resource_name": "Example Invoices MCP",
}
_jwks = PyJWKClient(f"{ISSUER}/.well-known/jwks.json") # use jwks_uri from AS metadata
class AuthError(Exception):
def __init__(self, status: int, www_authenticate: str):
super().__init__(status)
self.status, self.www_authenticate = status, www_authenticate
def _challenge(error=None, scope=None, description=None) -> str:
parts = [f'resource_metadata="{PRM_URL}"']
if error:
parts.insert(0, f'error="{error}"')
if scope:
parts.append(f'scope="{" ".join(sorted(scope))}"')
if description:
parts.append(f'error_description="{description}"')
return "Bearer " + ", ".join(parts)
def verify_access_token(auth_header, key=None) -> dict:
"""Return claims, or raise AuthError(401). `key` is only for tests."""
if not auth_header or not auth_header.startswith("Bearer "):
raise AuthError(401, _challenge(scope=BASE_SCOPES)) # first contact: no token
token = auth_header[len("Bearer "):].strip()
try:
typ = str(jwt.get_unverified_header(token).get("typ", "")).lower()
if typ not in ("at+jwt", "application/at+jwt"): # RFC 9068 section 4
raise jwt.InvalidTokenError("not an access token")
signing_key = key or _jwks.get_signing_key_from_jwt(token).key
return jwt.decode(
token, signing_key,
algorithms=ALLOWED_ALGS,
audience=RESOURCE, # RFC 8707 audience binding
issuer=ISSUER,
leeway=60,
options={"require": ["exp", "iss", "aud", "sub"]},
)
except jwt.InvalidTokenError as exc:
raise AuthError(401, _challenge("invalid_token", description=type(exc).__name__))
def authorize(claims: dict, method: str, tool=None) -> None:
"""Raise AuthError(403) with every missing scope in one challenge."""
granted = set(str(claims.get("scope", "")).split())
needed = set(BASE_SCOPES)
if method == "tools/call":
if tool not in TOOL_SCOPES:
raise AuthError(403, _challenge("insufficient_scope", description="unknown tool"))
needed |= TOOL_SCOPES[tool]
missing = needed - granted
if missing:
raise AuthError(403, _challenge("insufficient_scope", scope=needed))
ローカルでRS256署名したトークンを使って確認した結果は次のとおりです。
| テストしたトークン | 結果 | 理由 |
|---|---|---|
Authorization ヘッダーなし |
401(scope="mcp:basic" 付き) |
初回のチャレンジ |
| 正しい宛先の有効なトークン | 受け入れ | — |
aud が別サービス |
401 |
InvalidAudienceError |
aud が自分のURIを含むリスト |
受け入れ | RFC 9068は複数の宛先を認めている |
aud が自分のURI+末尾スラッシュ |
401 |
InvalidAudienceError(完全一致) |
| 期限切れ | 401 |
ExpiredSignatureError |
| 発行者が違う | 401 |
InvalidIssuerError |
typ: JWT(IDトークンの形) |
401 |
署名検証の前に拒否 |
alg: none |
401 |
InvalidAlgorithmError |
読み取りスコープで refund_invoice を呼ぶ |
403(scope="invoices:write mcp:basic") |
ステップアップのチャレンジ |
| 未知のツール | 403 |
fail closed |
本番で使う前に調整してほしい点:ISSUER とJWKSのURLは認可サーバーのメタデータから読み込む。認可サーバーがRFC 9068形式のトークンを出すなら、必須クレームに client_id・iat・jti も加える。スコープの階層を authorize に組み込む。内部の例外名を error_description に出したくなければ汎用の文言に変える。
6. curlで自分のサーバーを確認する
次の5つの確認で、MCPのOAuth設定の不備の大半は見つかります。ホスト名は自分のものに置き換えてください。
# 1. 認証なしの呼び出し:401と、resource_metadataを含むWWW-Authenticateが返るか
curl -si -X POST https://mcp.example.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | grep -iE '^HTTP|^www-authenticate'
# 2. Protected Resource Metadata:"resource" が正規URIと一致するか
curl -s https://mcp.example.com/.well-known/oauth-protected-resource/mcp | jq '{resource, authorization_servers, scopes_supported}'
# 3. 認可サーバーのメタデータ:issuerが一致し、S256があるか
curl -s https://auth.example.com/.well-known/oauth-authorization-server \
| jq '{issuer, code_challenge_methods_supported, client_id_metadata_document_supported, authorization_response_iss_parameter_supported}'
# 4. クエリ文字列のトークンが通らないか
curl -si -X POST "https://mcp.example.com/mcp?access_token=$TOKEN" \
-H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | head -1
# 5. 別のリソース向けに発行された有効なトークンが401になるか
curl -si -X POST https://mcp.example.com/mcp -H "Authorization: Bearer $TOKEN_FOR_OTHER_API" \
-H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | head -1
| 確認 | 合格 | 不合格の意味 |
|---|---|---|
| 1 | 401 と resource_metadata="..." |
クライアントが認可サーバーを見つけられない |
| 2 | resource が使ったURLと完全一致 |
クライアントはメタデータを破棄しなければならない |
| 3 | issuer が一致し、S256 がある |
クライアントは処理を中止しなければならない |
| 4 | 401 |
トークンがプロキシやCDNのログに漏れる |
| 5 | 401 |
宛先の拘束がなく、他サービス向けのトークンが通る |
最後は実際のクライアントでも接続して確かめましょう。Claude、Cursor、VS CodeなどへのサーバーURLの設定方法はリモートMCPクライアント設定チートシートにまとめています。
7. ステータスコードとヘッダーの早見表
| 状況 | ステータス | WWW-Authenticate |
|---|---|---|
| トークンなし | 401 |
Bearer resource_metadata="...", scope="<基本スコープ>" |
| 不正・期限切れ・宛先違いのトークン | 401 |
Bearer error="invalid_token", resource_metadata="..." |
| 有効なトークンだがスコープ不足 | 403 |
Bearer error="insufficient_scope", scope="<必要なスコープすべて>", resource_metadata="..." |
| 認可リクエストの形式が不正 | 400 |
— |
8. 実装チェックリスト(15項目)
サーバー側
- [ ] 末尾スラッシュなしの正規URIを1つに決め、メタデータ・ドキュメント・宛先の検証で同じものを使っている
- [ ] Protected Resource Metadataをパス付きのURLで公開している(1ホスト1サーバーならルートにも)
- [ ] メタデータの
resourceが正規URIと完全一致している - [ ]
scopes_supportedは基本スコープだけで、offline_accessを含まない - [ ] MCPエンドポイントへの認証なしリクエストには、すべて
resource_metadataとscope付きの401を返す - [ ] トークンは
Authorizationヘッダーからしか受け付けない - [ ]
typ・署名・許可アルゴリズム・iss・aud・expを毎リクエスト検証している - [ ] スコープ不足には、必要なスコープをすべて含めた
403 insufficient_scopeを1回で返す - [ ] 未知のツールは拒否し、スコープの階層を扱っている
- [ ] 下流APIはサーバー自身の資格情報(シークレットとして保管)で呼んでいる
認可サーバー側
- [ ] メタデータの
issuerが発行者URLと完全一致している - [ ]
code_challenge_methods_supportedにS256がある - [ ]
resourceパラメーターがトークンのaudに入る - [ ] Client ID Metadata Documentsに対応している(DCRは古いクライアント向けのみ)
- [ ] アクセストークンが短命で、公開クライアントのリフレッシュトークンをローテーションしている
9. よくあるエラー(症状→原因→対処)
| 症状 | 原因 | 対処 |
|---|---|---|
| クライアントが「認可サーバーが見つからない」と言う | メタデータがない、または 401 に resource_metadata がない |
RFC 9728のメタデータを公開し、ヘッダーを付ける |
| メタデータを取得した後でクライアントが止まる | resource がクライアントの使ったURLと違う(http と https、末尾スラッシュの違いが多い) |
resource を正規URIと1文字単位で一致させる |
| クライアントがログインを始めない | 認可サーバーのメタデータに code_challenge_methods_supported がない |
認可サーバーでPKCE S256 を有効にする |
ログインはできるが、すべての呼び出しが 401 |
aud がIdPの既定値(クライアントIDなど)で、自分のURIではない |
認可サーバーが resource を反映するよう設定するか、専用のaudienceを割り当てる |
| 新しいツールを使うたびにログインを求められる | サーバーがスコープを1つずつ要求している | 操作に必要なスコープを1回の 403 にまとめる |
| 社内の別APIのトークンがMCPサーバーで通る | 宛先を検証していない | aud を検証する(仕様上MUST) |
| 上流APIのログにエンドユーザーのトークンが出る | トークンの転送 | サーバー自身の上流用資格情報を使う |
| OIDCプロバイダーでクライアント登録が失敗する | application_type が合わないDCR |
Client ID Metadata Documentsを優先する。DCRを使うならローカルのリダイレクトURIには native を送る |
10. NodeFlareならこう解決できる
NodeFlareは、MCPサーバーをプロキシの後ろでホストし、リクエストがあなたのコードに届く前に認証と認可を行います。この記事で挙げたリスクと、ドキュメントに記載された機能の対応は次のとおりです。
| この記事のリスク | NodeFlareの機能 | 内容 |
|---|---|---|
| クライアントがログイン方法を見つけられない(ステップ3〜4) | プロキシでのOAuthディスカバリ | プロキシがOAuthのディスカバリメタデータ(RFC 8414 / RFC 9728)を配信するため、標準準拠のMCPクライアントは認証を自動で見つけられます。Bearerトークンは mcp_* のAPIキーか、プロキシが検証するOAuthアクセストークンのどちらか |
| 検証されない・推測されるトークン(ステップ5) | ハッシュ化したAPIキーとロックアウト | APIキーはハッシュ化して保存し、Redisキャッシュ(5分TTL)付きで照合。同じIPから誤ったキーが繰り返されると、そのIPを10分間ロック |
| 権限が広すぎるトークン(ステップ6) | ツール単位のスコープ | 転送前に、呼び出し元のスコープを正確なMCPメソッドと対象に照合:tools:list、tools:call、tools:call:<ツール名>(例:tools:call:get_weather)、resources:read:<uri>、prompts:get:<name>。拒否時はJSON-RPCエラーを返し、サーバーには届きません |
| 呼べないツールまで見えてしまう | tool_list_filter_by_scope |
呼び出し元が実行できないツールを tools/list から隠す |
| 漏れたトークンが別のネットワークから使われる | 送信元IPの許可リスト | ポリシーを restricted にしてIPv4/IPv6のCIDRを登録。認証より前、VMの起動より前に判定し、リストが空なら全拒否(fail closed)。ネットワークアクセス制限はPro・Teamプランの機能 |
| 上流APIへのトークン転送(ステップ7) | 暗号化されたシークレット | 上流の資格情報は環境変数として保存。保存時に暗号化され、VM起動時にだけ注入され、ビルド引数・イメージレイヤー・ログには出ません |
| 他のサーバーからシークレットに届く | microVM分離 | サーバーごとに専用のFirecracker microVMで動作 |
| 401や403の記録が残らない | リクエストログ | 各リクエストの caller_kind(api_key / oauth / anonymous)、client_ip、tool_name、http_status、outcome(scope_denied など)を記録。引数の値は保存しません。保存期間はFreeが7日、Pro/Teamが30日 |
すでに自前の認可サーバーと5章のような検証を実装済みなら、auth_enabled = false にすれば、NodeFlareは呼び出し元の資格情報をそのままサーバーへ転送します。この場合、この記事の各ステップはすべてあなたのサーバーの担当になります。
NodeFlareがしないこと:IdPの設定は代行しません。また、モデル内部でのプロンプトインジェクションは防げません。狭いスコープ、IP許可リスト、リクエストログで、トークンが悪用されたときの被害を小さくするのが役割です。
最小の始め方:
- MCPサーバーをデプロイする(初期状態で非公開)。
- 人間以外の呼び出し元には、必要な
tools:call:<ツール名>だけを付けたmcp_*APIキーを1つずつ発行し、人はOAuthで接続させる。 - 1週間後にリクエストログで
scope_deniedと401を確認し、スコープを絞り込む。
1サーバーまでは無料で試せます:NodeFlareを始める
11. よくある質問(FAQ)
MCPでOAuthは必須ですか?
必須ではありません。MCP仕様では認可はOPTIONALです。ただし、HTTP系のサーバーが認可に対応する場合は、OAuth 2.1ベースのMCP認可仕様に従うべき(SHOULD)とされています。stdioのサーバーは従わず、環境変数などから資格情報を取得します。
MCPのProtected Resource Metadataとは何ですか?
RFC 9728で定義されたJSON文書で、「このMCPサーバー向けのトークンはどの認可サーバーが発行するか」をクライアントに伝えます。2026-07-28版の仕様では、MCPサーバーはこれを実装しなければなりません(MUST)。/.well-known/oauth-protected-resource にMCPのパスを付けた場所で公開し、401 レスポンスからも指し示します。
resource_metadataのURLはどこを指せばよいですか?
そのMCPエンドポイント専用のRFC 9728文書を指します。/mcp で動くサーバーなら https://mcp.example.com/.well-known/oauth-protected-resource/mcp です。ヘッダーがない場合、クライアントはまずパス付きのURL、次にルートのURLを探します。
MCPのアクセストークンはどう検証すればよいですか?
許可するアルゴリズムを固定したうえで署名を確かめ、発行者・有効期限・トークン種別を確認し、何より宛先(audience)に自分の正規URIが含まれることを確認します。失敗したら 401 と error="invalid_token" を返します。不透明トークンならRFC 7662のイントロスペクションで同じ確認をします。
MCPのOAuthにおけるresourceパラメーターとは?
RFC 8707で定義された、「どのMCPサーバー向けのトークンか」を示すパラメーターです。クライアントは認可リクエストとトークンリクエストの両方で、サーバーの正規URIを送らなければなりません(MUST)。認可サーバーがこれをトークンの宛先に入れることで、MCPサーバーは他サービス向けのトークンを拒否できます。
MCPでDynamic Client Registrationはまだ必要ですか?
必要ありません。2026-07-28版ではDCRは非推奨で、後方互換のためだけに残されています。クライアントと認可サーバーは、HTTPSのURLをクライアントIDとして使うClient ID Metadata Documentsに対応すべき(SHOULD)とされています。
トークンのスコープが足りないとき、MCPサーバーは何を返すべきですか?
403 Forbidden と、WWW-Authenticate: Bearer error="insufficient_scope"、その操作に必要なスコープをすべて並べた scope、そして resource_metadata を返します。仕様は必要なスコープを1回のチャレンジにまとめるべき(SHOULD)としており、クライアントは1往復でステップアップできます。
ユーザーのMCPトークンを自社のバックエンドAPIに転送してもよいですか?
いけません。これはトークンの転送(token passthrough)で、仕様で禁止されています(MUST NOT)。バックエンドは、サーバー自身のシークレットか、バックエンドの認可サーバーから取得した別のトークンで呼んでください。
リモートMCPサーバーにはOAuthとAPIキーのどちらを使うべきですか?
人が自分のアカウントで接続するならOAuthです。同意と失効をユーザー単位で管理できます。エージェント、定期ジョブ、CIのようにブラウザでログインできない呼び出し元には、スコープを絞ったAPIキーを使います。本番では両方に対応するサーバーも多くあります。
MCP用に別の認可サーバーを立てる必要がありますか?
必ずしも必要ではありません。仕様は、認可サーバーをMCPサーバーと同居させることも別サービスにすることも認めており、多くのチームは既存のIdPを流用しています。重要なのは、PKCE S256、ディスカバリメタデータ、resource パラメーター、できればClient ID Metadata Documentsに対応していることです。
まとめ
サーバー側のMCP OAuthは、守るべき厳密な動作のリストです。正規URIを1つに決め、それと一致するProtected Resource Metadataを公開し、そこを指す 401 チャレンジを返し、毎回宛先まで含めてトークンを検証し、ツール単位のスコープと1回にまとめた 403 のステップアップを用意し、上流の呼び出しには自前の資格情報を使う。認可サーバーはPKCE S256、resource パラメーター、Client ID Metadata Documentsに対応したものを選び、URLを人に渡す前に6章の5つの curl 確認を実行してください。tool poisoningやサプライチェーン、Egress制御を含むMCPセキュリティの全体像は、MCPセキュリティのベストプラクティスとチェックリストにまとめています。
参考資料(一次情報)
- MCP Specification 2026-07-28: Authorization
- MCP Specification 2026-07-28: Authorization Server Discovery
- MCP Specification 2026-07-28: Client Registration
- MCP Specification 2026-07-28: Authorization Security Considerations
- RFC 9728: OAuth 2.0 Protected Resource Metadata
- RFC 8707: Resource Indicators for OAuth 2.0
- RFC 9068: JWT Profile for OAuth 2.0 Access Tokens
- RFC 9207: OAuth 2.0 Authorization Server Issuer Identification
- MCP TypeScript SDK v2: Require authorization
- MCP Python SDK: Authorization
- NodeFlare Docs
