Kong GatewayのMCP OAuth2 Pluginは3.13まではサポートするトークン検証方法がIntrospectionしかなかった。
日本の企業でよく使われるEntra IDなんかはIntrospectionをサポートしていないため、MCP OAuth2 PluginはEntra IDと組み合わせて使うことができなかった。
しかし、MCP OAuth2 Plugin 3.14からはJWT検証もサポートされるようになったため、Entra IDと組み合わせて使うことができるようになった。
今回はこれを検証してみる。
前提知識
MCP OAuth2 Pluginを使った認可フローは以下のようになる。
アクセストークンはデフォルトで上流サービスに転送されない設計となっており、トークン盗聴や confused deputy 攻撃(権限を持つ仲介者が悪用されて不正操作される攻撃)を防止している。
各フェーズで使われる値の詳細は以下の通り。
-
メタデータ取得
- KongはProtected Resource Metadataに認可サーバーのIssuer URLを埋め込んで返す
- 必要な値: Issuer URL (
https://login.microsoftonline.com/{tenant_id}/v2.0)
-
トークン取得
- MCP ClientはEntra IDのトークンエンドポイントに
client_id/client_secret/scopeを送りJWTを取得する - 必要な値: テナントID、クライアントID、クライアントシークレット、スコープ
- MCP ClientはEntra IDのトークンエンドポイントに
-
JWT検証
- JWKSエンドポイント (
https://login.microsoftonline.com/{tenant_id}/discovery/v2.0/keys) から公開鍵を取得し署名を検証 - iss, aud, exp等も検証
- JWKSエンドポイント (
-
MCPサーバへ転送(フロー図 フェーズ3 — 有効時)
- Kongはトークンを除去し、リクエストをMCPサーバーへ転送する
- 必要な値: サービスURL(MCPサーバーのエンドポイント)
なお、フローの通りMCP OAuth2 Pluginは認可のみであり認証は行わず、トークンの取得はクライアント側で実施することになる。
ただConsumerとのマッピングは可能で、consumer_byで指定したConsumer情報とsubクレームを照合することもできる(今回は扱わない)。
MCP Proxy Pluginと組み合わせた場合、MCP OAuth2 PluginはMCP Proxy Pluginの前段で動作し、トークン検証後にMCPとAPIの変換を行う。
事前準備
以下は既にあるものとする。
- Kong Konnect (CPとDP)
- Entra IDテナントとアプリ登録
- クライアントID / クライアントシークレットを控えておく
-
accessTokenAcceptedVersionは2に設定する(マニフェスト->AAD Graphアプリマニフェストで設定可能)
- MCPサーバー(またはAPIサーバーを用意し、MCP Proxy PluginでMCPサーバー化する)
- MCPクライアント
今回の検証ではDPとhttpbinをローカル環境で起動して検証した。
httpbinに対してはMCP Proxy Pluginを使ってMCPサーバー化する。
MCPクライアントはリクエスト・レスポンスの中身を可視化するためにcurlを利用した。
検証
検証手順としては以下となる。
- MCP OAuth2 Pluginの設定
2.検証用のトークンの取得 - MCPリクエストの送信とレスポンスの確認
また最初に検証に使う環境変数を設定する。
# deck
export DECK_KONNECT_ADDR=https://us.api.konghq.com
export DECK_KONNECT_TOKEN=<your-konnect-token>
export DECK_KONNECT_CONTROL_PLANE_NAME=<your-control-plane-name>
# Entra ID
export ENTRA_TENANT_ID=<your-tenant-id>
export ENTRA_CLIENT_ID=<your-client-id>
export ENTRA_CLIENT_SECRET=<your-client-secret>
export ENTRA_APP_ID_URI=${ENTRA_CLIENT_ID}
export ENTRA_SCOPE=api://${ENTRA_APP_ID_URI}/.default
# MCP OAuth2 Plugin
export DECK_MCP_RESOURCE_URL=api://${ENTRA_APP_ID_URI}
export DECK_AUTHORIZATION_SERVER_URL=https://login.microsoftonline.com/${ENTRA_TENANT_ID}/v2.0
export DECK_JWKS_ENDPOINT_URL=https://login.microsoftonline.com/${ENTRA_TENANT_ID}/discovery/v2.0/keys
1. MCP OAuth2 Pluginの設定
以下のdeck用YAMLを用意して設定する。
_format_version: "3.0"
services:
- name: mcp-oauth-entraid-service
protocol: http
host: httpbin
path: /anything
port: 80
routes:
- name: mcp-oauth-entraid-route
paths:
- /mcp
- /.well-known/oauth-protected-resource
protocols:
- http
plugins:
- name: ai-mcp-oauth2
config:
resource: ${{ env "DECK_MCP_RESOURCE_URL" }}
authorization_servers:
- ${{ env "DECK_AUTHORIZATION_SERVER_URL" }}
jwks_endpoint: ${{ env "DECK_JWKS_ENDPOINT_URL" }}
metadata_endpoint: /.well-known/oauth-protected-resource
insecure_relaxed_audience_validation: true
- name: ai-mcp-proxy
config:
mode: conversion-listener
tools:
- name: httpbin-anything
description: Send a GET request to httpbin /anything endpoint via MCP tools/call
method: GET
設定の意味は以下となる。
paths:
- /mcp
- /.well-known/oauth-protected-resource
通常のRouteと異なり、最初の認可サーバの取得用に/.well-known/oauth-protected-resourceもルーティングしてあげる必要がある。
このパスを設定した上でMCP OAuth2 Pluginを有効にすることで、クライアントは最初に/.well-known/oauth-protected-resourceにアクセスして認可サーバのURLを取得することができるようになる。
resource: ${{ env "DECK_MCP_RESOURCE_URL" }}
authorization_servers:
- ${{ env "DECK_AUTHORIZATION_SERVER_URL" }}
jwks_endpoint: ${{ env "DECK_JWKS_ENDPOINT_URL" }}
metadata_endpoint: /.well-known/oauth-protected-resource
insecure_relaxed_audience_validation: true
各値の意味は以下となる。
-
resource: トークンのaudienceクレームと照合する値。Entra IDの場合はApplication ID URIを指定するのが一般的だが、検証の結果Client CredentialsフローではaudienceはGUID形式で返却されるため、insecure_relaxed_audience_validation: trueを設定してaudience検証をスキップするのが現実的な解となる。 -
authorization_servers: 認可サーバーのURL。トークンのissクレームと照合するために使用される。 -
jwks_endpoint: JWTの署名検証に使う公開鍵が置いてあるURL -
metadata_endpoint: クライアントが認可サーバの情報を取得するためのエンドポイント。これを設定することでプラグインがこのパスをメタデータエンドポイントとして認識し、認証をスキップしてメタデータを返すようになる。 -
insecure_relaxed_audience_validation:trueに設定するとaudience検証をスキップする。Entra IDのClient CredentialsフローではaudienceがGUID形式で返却されるように見えているため、これをURL形式と照合することができない。今回はaudience検証をスキップするためにtrueに設定する。
mode: conversion-listener
tools:
- name: httpbin-anything
description: Send a GET request to httpbin /anything endpoint via MCP tools/call
method: GET
MCP Proxy Pluginの設定。今回はhttpbinの/anythingエンドポイントにGETリクエストを送るツールを定義している。
上記のYAMLを作成したらKongに設定を反映する。
deck gateway sync ./deck.yaml
2. 検証用のトークンの取得
Client Credentialsフローでアクセストークンを取得する。
TOKEN=$(curl -s -X POST \
"https://login.microsoftonline.com/${ENTRA_TENANT_ID}/oauth2/v2.0/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=${ENTRA_CLIENT_ID}" \
-d "client_secret=${ENTRA_CLIENT_SECRET}" \
-d "scope=${ENTRA_SCOPE}" \
| jq -r '.access_token')
echo $TOKEN
取得したJWTのクレームを確認する場合は以下で確認できる(ペイロード部分をデコード)。
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq .
デフォルトのトークンの有効期限は1時間(3600秒)となっているため、検証が長引く場合は適宜トークンを再取得すること。
3. MCPリクエストの送信とレスポンスの確認
認可フローに従ってアクセスする。
まず最初に/.well-known/oauth-protected-resourceにアクセスして、Issuer URLが正しく返ってくることを確認する。
curl -s http://localhost:8000/.well-known/oauth-protected-resource | jq .
{
"resource": "http://localhost:8000/mcp",
"authorization_servers": [
"https://login.microsoftonline.com/<your-tenant-id>/v2.0"
]
}
authorization_serversには先程トークンの取得で利用したEntra IDのテナントIDつきURLが返ってきている事が分かる。
ここではトークンは取得済みなので、本来トークンを取得する手順をスキップして次にMCPリクエストを送信してみる。
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8000/mcp/aaa | jq .
問題なければ以下のような結果が返ってくるはずだ。
{
"args": {},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Connection": "keep-alive",
"Host": "httpbin",
"User-Agent": "curl/8.7.1",
"X-Forwarded-Host": "localhost",
"X-Forwarded-Path": "/mcp/aaa",
"X-Forwarded-Prefix": "/mcp",
"X-Kong-Request-Id": "1fef1576c67bf81ff92a003d2a263540"
},
"json": null,
"method": "GET",
"origin": "172.22.0.1",
"url": "http://httpbin/anything/aaa"
}
試しに誤ったトークンを設定して実行する。
TOKEN=invalid
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8000/mcp/aaa | jq .
以下の結果となった。
{
"message": "Invalid or inactive token"
}
またKong DPのログには以下のようなJWTの検証で失敗したことを示すエラーが出ているはずだ。
2026/04/21 01:16:17 [notice] 2915#0: *4554 [kong] auth.lua:841 [ai-mcp-oauth2] invalid token detected during JWKS verification: failed to decode JWT: invalid JWT format, client: 172.22.0.1, server: kong, request: "GET /mcp/aaa HTTP/1.1", host: "localhost:8000", request_id: "2650662e51a8f674bf2d2529e89e980a"
2026/04/21 01:16:17 [debug] 2915#0: *4554 [kong] response.lua:1051 [ai-mcp-oauth2] exiting with status code: 401 and message: Invalid or inactive token
上手くJWTの検証ができていることが確認できた。
これまではMCP+OAuth2の構成を検証したい時にEntra IDでは検証できないという課題があったが、MCP OAuth2 Plugin 3.14からはEntra IDもサポートされたため、より幅広い環境での検証が可能になったことが確認できた。
トラブルシューティング
accessTokenAcceptedVersionの設定漏れでトークン検証に失敗
accessTokenAcceptedVersionが2以外になっていると、以下のようなエラーが出てトークンが無効とされる。
2026/04/21 01:08:42 [notice] 2916#0: *4035 [kong] auth.lua:841 [ai-mcp-oauth2] invalid token detected during JWKS verification: token structure validation failed: invalid issuer (https://sts.windows.net/<your-tenant-id>/) was specified for access token, https://login.microsoftonline.com/<your-tenant-id>/v2.0 was expected, client: 172.22.0.1, server: kong, request: "GET /mcp HTTP/1.1", host: "localhost:8000", request_id: "d8629544d56354285c8bbb1fb14e761c"
2026/04/21 01:08:42 [debug] 2916#0: *4035 [kong] response.lua:1051 [ai-mcp-oauth2] exiting with status code: 401 and message: Invalid or inactive token
上記エラーが出た場合はaccessTokenAcceptedVersionが2になっているかを確認すること。
resourceにURLを設定してもaudience検証が失敗する
Kongのresourceにhttp://localhost:8000/mcpのようなURLを設定しても、以下のエラーが返ることがある。
{"message": "Invalid audience in token"}
Entra IDが発行するJWTのaudクレームはURLではなく、アプリ登録のApplication IDやApplication ID URIになる。Kongはaudをresourceの値と照合するため、URLのまま設定しても一致しない。
またGUIDをそのままresourceに設定しようとすると以下のバリデーションエラーが発生する。
validation error: missing host in url
KongのresourceフィールドはURL形式が必須のため、GUIDは直接設定できない。
Entra IDはデフォルトでapi://<client-id>というApplication ID URIを持つため、これが利用できるようにも見えるが、Entra IDのClient CredentialsフローではaudはGUID形式で返却され、スコープをapi://<client-id>/.defaultと指定してもaudはGUIDのままであり、api://形式にはならなかった。
これが返ってくるようであればMCP OAuth2 Pluginではaudが実質検証できない。
同様の状態になる場合は検証をスキップするためのinsecure_relaxed_audience_validation: trueを設定して本問題を回避すれば動作させることが可能となる。