本記事は元々 blog.logto.io に掲載されたものです。
数日前(2025年6月18日)、MCP(Model Context Protocol)チームは MCP 仕様書(2025-06-18) の最新版をリリースしました。このアップデートでは、認証仕様における重要な変更が含まれています。MCP サーバーは今後、認可サーバー として アクセストークン を発行しなくなります。代わりに、アクセストークンを受け入れ、リソースサーバー としてリソースを提供する形となります。
MCp Auth(プラグアンドプレイな MCP サーバー認証ライブラリ)のメンテナーの 1 人として、私は本プロジェクトで最新版の MCP 認証仕様へ対応を行いました。実践経験を元に、最新仕様に準拠する MCP サーバーの認証機能をどのように実装できるかを解説します。
本記事では、以下の内容を扱います:
- 新しい MCP 認証仕様で MCP の認証がどのように動作するのか把握する
- MCP サーバーが MCP 認証仕様の要件に従い、リソースサーバーとして何を実装すべきか明確にする
- MCP サーバー向けの最新 MCP 認証仕様に合致した認証サポート実装ガイド
- MCP 認証実装時に見落としやすいポイントやセキュリティ課題を特定する
注意事項:
- 読者が JWT(JSON Web Token)の基礎知識を持っていることを前提としています。この記事では JWT の構造や署名検証等の基礎概念には深入りしません。詳しくは Auth Wiki - JWT を参照してください。
- MCP 認証仕様が依存する各 RFC についても深入りせず、要件を満たす実装のみ解説します。
- MCP クライアント・認可の実装や相互作用の詳細は取り上げません。これらは通常、LLM クライアントや MCP に対応した認可サーバープロバイダー側で実装されるものであり、MCP サーバー開発者が介入・干渉することはできません。具体的な情報は OAuth Client や Authorization Server を参照してください。
- 本記事で言及するアクセストークンは、市場で最も広く使われている JWT 形式を想定しています。他の種類のトークンについては、各認可サーバープロバイダーのドキュメントを参照してください。
MCP 認証はどのように機能するのか?
最新 MCP 認証仕様 に基づき、MCP 認証フローは次の図のように動作します:
-
MCP クライアントは
https://github-tools.com
の MCP サーバーからリソースを要求します。まだ認証していないため、このリクエストの HTTP 認可ヘッダーにはアクセストークンが含まれていません。 -
MCP サーバーはリクエストの認可ヘッダーからアクセストークンを取得できません。HTTP 401 エラーを MCP クライアントに返却し、エラーレスポンスには WWW-Authenticate ヘッダーとして MCP サーバーのリソースメタデータ(
resource_metadata
フィールドの値)の URL を含めます。 -
MCP クライアントは返却された WWW-Authenticate ヘッダーから
resource_metadata
の値(例:https://github-tools.com/.well-known/oauth-protected-resource
)を取り出し、このアドレスからリソースサーバーとしての MCP サーバーが提供するリソースメタデータを取得します。このメタデータにはauthorization_servers
やscopes_supported
などの情報が含まれており、MCP サーバーへアクセスするためにどの認可サーバーからどんな権限付きトークンを取得すべきかを MCP クライアントは理解できます。
4-8. MCP クライアントはリソースメタデータから得た認可サーバーメタデータ URL を元に認可サーバーメタデータ情報をリクエストし、OAuth 2.1 認可フローを通じて認可サーバーからアクセストークンを取得します。
-
MCP クライアントは取得したアクセストークンを認可ヘッダーに付与して再度 MCP サーバーへリソースアクセスリクエストを送信します。
-
MCP サーバーはトークンが有効であることを確認後、リクエストされたリソースを返します。この後も有効なトークンを使って MCP クライアントと MCP サーバー間で通信を継続できます。
次に、MCP 認証ワークフローに基づき、サーバー側の認証メカニズム実装方法をステップごとに解説します。
未認証リクエストの処理:401 エラーと WWW-Authenticate ヘッダーの返却
上記のフローの通り、MCP クライアントがアクセストークンなしで MCP サーバーにリクエストを送信した場合、サーバーはリソースメタデータの URL を含んだ WWW-Authenticate
ヘッダー付きの 401 Unauthorized エラーを返却する必要があります。
MCP 認証仕様のエラーハンドリング規定 に従い、MCP クライアントのリクエストにアクセストークンが含まれない場合だけでなく、MCP サーバーが無効なアクセストークンを受信した場合も WWW-Authenticate
ヘッダー付き 401 エラーを返すべきです。
ここで、401 エラーを返却するタイミングが分かったら、どのように WWW-Authenticate
ヘッダーを構築すれば良いのでしょうか?
RFC9728 Section 5.1 によれば、 WWW-Authenticate
ヘッダーに resource_metadata
パラメーターを含めて、保護リソースメタデータの URL を示します。
基本形は次のようになります:
WWW-Authenticate: Bearer resource_metadata="<metadata-url>"
ここで Bearer
は認証スキームであり、OAuth 2.0 による保護リソースへのアクセスには Bearer トークンが必要であることを意味します(MDN ドキュメント 参照)。続く resource_metadata
パラメーターの値は MCS サーバーが提供するリソースメタデータエンドポイントの完全な URL です。
コード例:
// アクセストークンが無効または欠落と判定したとき
function sendUnauthorizedResponse(res) {
const metadataUrl = "https://github-tools.com/.well-known/oauth-protected-resource";
res.set('WWW-Authenticate', `Bearer resource_metadata="${metadataUrl}"`);
res.status(401).json({ error: 'unauthorized' });
}
MCP クライアントは上記のような 401 エラーを受信した場合、WWW-Authenticate
ヘッダー内の resource_metadata
URL から MCP サーバーのリソースメタデータを取得し、記載の情報に基づいて指定された認可サーバーに認可リクエストを送信して、MCP サーバーアクセス用のアクセストークンを取得します。
これで 401 エラー返却のタイミングや resource_metadata
URL の必要性が分かりました。次はこのリソースメタデータ URL の組み立て方や中身について説明します。
リソースメタデータディスカバリ機構の実装
MCP 認証フローでは、MCP クライアントが 401 エラーを受信した直後、すぐに MCP サーバーにリソースメタデータのリクエストを送信します。従って、MCP サーバー(リソースサーバー)はリソースメタデータディスカバリ機構を実装する必要があります。
メタデータエンドポイントの URL パス決定
OAuth システムでは、URL でリソースアドレス(リソースインジケーター)を識別します。RFC9728 に従い、リソースメタデータは /.well-known
パス配下でホストします。
MCP サーバー(例: https://github-tools.com
)が 1 つのサービスのみを提供している場合、メタデータエンドポイントは:
https://github-tools.com/.well-known/oauth-protected-resource
1 つのホストで複数の MCP サービスを提供する場合、それぞれ独立したメタデータエンドポイントを用意します。たとえば、https://api.acme-corp.com
で次のサービスがある場合:
-
https://api.acme-corp.com/github
- GitHub 連携サービス -
https://api.acme-corp.com/slack
- Slack 連携サービス -
https://api.acme-corp.com/database
- データベースクエリサービス
それぞれのメタデータエンドポイント:
https://api.acme-corp.com/.well-known/oauth-protected-resource/github
https://api.acme-corp.com/.well-known/oauth-protected-resource/slack
https://api.acme-corp.com/.well-known/oauth-protected-resource/database
この設計により、各サービス毎に異なる権限制御や認可サーバー構成が可能になります。例えば:
- GitHub サービスは GitHub OAuth サーバー+
github:read
、github:write
権限が必要 - Slack サービスは Slack OAuth サーバー+
slack:channels:read
、slack:messages:write
権限が必要 - Database サービスは社内認可サーバー+
db:query
権限が必要
まとめると、メタデータエンドポイントの URL 規則:
{server-address}/.well-known/oauth-protected-resource[/service-path]
このようにリソース識別子からメタデータエンドポイント URL を組み立てできます:
export const createResourceMetadataEndpoint = (resource: string) => {
const resourceUrl = trySafe(() => new URL(resource));
if (!resourceUrl) {
throw new TypeError(`Invalid resource identifier URI: ${resource}`);
}
// パスが空または '/' の場合、ベース直下にエンドポイント
if (resourceUrl.pathname === '/') {
return new URL(resourceMetadataBasePath, resourceUrl.origin);
}
// それ以外は well-known パス配下に該当サービスパスを付加
return new URL(`${resourceMetadataBasePath}${resourceUrl.pathname}`, resourceUrl.origin);
};
リソースメタデータレスポンスの作成
エンドポイント URL パスを決めたら、このエンドポイントが RFC9728 に準拠した JSON 形式のメタデータを返すようにします。
実装では、主に次の 4 つのコアフィールドを意識してください。
まず resource
フィールドはリソース識別子であり、MCP クライアントがアクセスしたいリソースアドレスと一致させる必要があります。
次に authorization_servers
フィールドは、MCP クライアントがどの認可サーバーへアクセストークンを要求すべきか指定する配列です。OAuth 2.0 Protected Resource Metadata(RFC 9728)では任意ですが、MCP 認証仕様では必須です。この情報が無ければ MCP クライアントは OAuth フローを完遂できません。
scopes_supported
フィールドはリソースサーバーがサポートするすべての権限スコープを列挙します。
最後に bearer_methods_supported
フィールドは、このサーバーがアクセストークン受け取りにサポートするメソッド種別を記載します。通常は ["header"]
とし、認可サーバーから取得したトークンを HTTP Authorization ヘッダーで MCP サーバーに送信するよう MCP クライアントに示します。
具体例。https://github-tools.com
MCP サーバー向けのリソースメタデータ:
{
"resource": "https://github-tools.com",
"authorization_servers": [
"https://auth.github-tools.com"
],
"scopes_supported": [
"github:read",
"github:write",
"repo:admin"
],
"bearer_methods_supported": [
"header"
]
}
この設定は MCP クライアントに、https://github-tools.com
を利用するためには https://auth.github-tools.com
認可サーバーから github:read
、github:write
、repo:admin
などの権限付きトークンを取得し、そのトークンを HTTP Authorization ヘッダーで送信すべきと伝えています。
一般的な用途では、この 4 つだけで十分 MCP クライアントが正常動作します。より複雑な設定は RFC9728 の全項目リストを参考にしてください。
アクセストークンの検証
MCP クライアントが認可サーバーからアクセストークンを発行された後、そのトークンを MCP サーバーへ添えてアクセスします。
このとき、MCP サーバーが意識すべき検証ポイント:
- 基本のトークン検証は MCP サーバー側の設定値準拠で行い、トークン内情報から認可サーバーを特定しないこと
- トークンの audience が自身(MCP サーバー)かどうかの検証
- scope の検証
MCP サーバー設定を使ったアクセストークン検証
MCP サーバーがアクセストークンを受信したとき、多くの場合トークン内部に認可サーバー情報(issuer
)があるため、その情報から自動的に検証先サーバーを決めようとしがちです。特に MCP サーバーのリソースメタデータで複数の認可サーバーが設定されている場合:
{
"resource": "https://github-tools.com",
"authorization_servers": [
"https://auth1.github-tools.com",
"https://auth2.github-tools.com"
],
"scopes_supported": [
"github:read",
"github:write",
"repo:admin"
],
"bearer_methods_supported": [
"header"
]
}
このとき、未検証トークンから issuer
を抽出して該当認可サーバーのデータで検証します。しかし攻撃者が悪意の認可サーバーで偽アクセストークンを発行した場合、その issuer
に基づいて(正当認可サーバーと見なして)検証してしまうと、不正トークンも通ってしまいます。
正しい検証方法:
- 認可サーバー 1 つの場合:サーバー側設定値のみでトークン検証
- 複数の場面:未検証トークンから
issuer
を抽出し、MCP サーバー設定値の中から該当認可サーバーを探して検証。見つからない場合は無効とする
トークン検証時は issuer
を厳格に検証してください。
jose JWT 検証ライブラリ例:
import { jwtVerify, createRemoteJWKSet } from 'jose';
// MCP サーバー設定の認可サーバー一覧(リソースメタデータと一致)
const configuredAuthServers = [
'https://auth1.github-tools.com',
'https://auth2.github-tools.com'
];
async function validateAccessToken(token, resourceIdentifier) {
try {
// 1. 未検証 JWT の payload から issuer を取得
const [header, payload] = token.split('.');
const decodedPayload = JSON.parse(Buffer.from(payload, 'base64url').toString());
const issuer = decodedPayload.iss;
// 2. issuer がサーバー設定一覧にあるかチェック
if (!configuredAuthServers.includes(issuer)) {
throw new Error('Token issuer not in configured authorization servers');
}
// 3. 対応する認可サーバーの JWKS で検証
const jwks = createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`));
// 4. JWT 検証(issuer/audience 厳密検証)
const { payload: verifiedPayload } = await jwtVerify(token, jwks, {
issuer: issuer, // issuer を厳密チェック
audience: resourceIdentifier, // audience が自身か判定
clockTolerance: 30 // 30秒の時計誤差許容
});
return {
valid: true,
payload: verifiedPayload
};
} catch (error) {
console.error('Token validation failed:', error.message);
return {
valid: false,
error: error.message
};
}
}
トークン audience の検証
MCP 認証仕様内「Token Audience Binding and Validation」 の規定で、クライアントがアクセストークンを認可サーバーにリクエストする際、どのリソース用なのか resource
パラメーターで明示し、サーバー側も受信したトークンの audience(aud)が自身かどうか必ず検証すべきとされています。
この仕組みは Resource Indicators for OAuth 2.0(RFC 8707)に準拠します。
要点ワークフロー:
-
クライアントは認可サーバーへのアクセストークンリクエスト時に
resource
パラメーターでリソースサーバー(例:https://github-tools.com
)を指定 -
認可サーバーは発行時に audience フィールド(JWT なら
aud
claim)に上記リソースを埋め込み -
MCP サーバーは受信したトークンの audience が自身と一致するか検証
例: https://github-tools.com
MCP サーバーの場合
// ... 他の JWT 検証コード ...
async function validateTokenAudience(token, resourceIdentifier) {
try {
// ... JWKS 取得/準備 ...
// audience 必須指定で JWT 検証
const { payload } = await jwtVerify(token, jwks, {
// ... 他のパラメーター ...
audience: resourceIdentifier, // ここが 'https://github-tools.com'
// ... 他のパラメーター ...
});
// OK: このトークンは本サーバー用として発行された
return { valid: true, payload };
} catch (error) {
if (error.code === 'JWTClaimValidationFailed' && error.claim === 'aud') {
// audience 不一致(他サービス用トークン)
console.error('Token audience mismatch:', error.message);
}
return { valid: false, error: error.message };
}
}
この audience チェックはトークン悪用防止の重要なセキュリティ技法です。これが抜けていると、攻撃者が他サービス用のアクセストークンで API に不正アクセスする恐れがあります。
scope の検証
一部 MCP サーバーは内部リソース管理時、ユーザー毎に異なる権限スコープを割り当てます。
従い、トークン検証が通った後も、 scope
claim に現在操作に必要なパーミッションが含まれるかチェックします:
// 今回の操作に github:read 権限が要る場合:
const requiredScope = 'github:read';
const tokenScopes = token.scope.split(' ');
if (!tokenScopes.includes(requiredScope)) {
// 403 Forbidden(権限不足)返却
}
MCP 認証仕様エラーハンドリング に沿って、トークンに必要スコープがなければ 403 Forbidden を返してください。
まとめ
本記事を通じて、MCP サーバーの最新仕様に準拠した認証機能が実装できるはずです。ポイントは「安全なトークン検証」「正しいメタデータ設定」「audience の厳格検証」の 3 つ。
もし MCP サーバー構築中であれば、MCP Auth ライブラリも利用してください。本記事で解説した機能すべて実装済で、迅速な認証統合が可能です。
質問は GitHub でいつでも歓迎です。MCP エコシステムを一緒に発展させましょう!