はじめに
2025年シー・エーアドバンスアドベントカレンダーの記事です!
通信フローは色々なところで紹介があるのですが、実際のログはどのようになっているのか?という事で記事にしてみました。
以下では通信ログを時系列で読み解き、どこで何が起きているかを整理します。
環境と前提
Keycloak + MCP サーバー構成で、MCP Inspector から OAuth 2.1 / OIDC フローを叩いたときの HTTP 通信を記録しました。
- OS / ローカル: WSL2 Ubuntu 24.04、Node.js v24.0.2(npm 11.6.4)
- 認可サーバー: Keycloak v26.2
http://localhost:9001(Realm:mcp) - リソース/MCP サーバー TypeScript v5.0.0:
http://localhost:3001 - クライアント: MCP Inspector v0.17.5
http://localhost:6274 - 通信ログの取得:Burp Suite v2025.10.6
シーケンス解説(ログ順)
全体の大まかな流れ
- ブラウザ(Inspector)が MCP リソースのメタデータを取得(
.well-known/oauth-protected-resource)。 - Keycloak の OIDC Discovery を正しいパスで取得(
/realms/mcp/.well-known/openid-configuration)。 - Dynamic Client Registration で一時クライアントを発行(201)。
- Authorization Code フロー開始 → Keycloak の同意画面へリダイレクト。
- 同意フォーム POST → 認可コード取得。
- 認可コード + PKCE の
code_verifierでトークンエンドポイントに POST → アクセストークン/IDトークンを受領。
1. リソース(MCP)メタデータ取得(OPTIONS/GET)
-
OPTIONS /.well-known/oauth-protected-resourceで CORS を確認(204)。 - 続く
GET /.well-known/oauth-protected-resourceでメタデータ取得(200)。
リクエスト例
OPTIONS /.well-known/oauth-protected-resource HTTP/1.1
Host: localhost:3001
Access-Control-Request-Method: GET
Access-Control-Request-Headers: mcp-protocol-version
Origin: http://localhost:6274
レスポンス例
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
Access-Control-Allow-Headers: mcp-protocol-version
GET 例
GET /.well-known/oauth-protected-resource HTTP/1.1
Host: localhost:3001
MCP-Protocol-Version: 2025-11-25
Origin: http://localhost:6274
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
{
"resource": "http://localhost:3001/",
"authorization_servers": ["http://localhost:9001/realms/mcp"],
"scopes_supported": ["profile"],
"resource_name": "Test MCP Server"
}
2. 認可サーバーのメタデータ取得
- ここで取得しているメタデータ(OIDC Discovery)は、認可サーバーが公開する「設定カタログ」です。
issuerやauthorization_endpoint、token_endpoint、registration_endpointなど、クライアントが動的に接続先を知るための一覧が含まれます。 - 動的取得が必要な理由: 環境ごとにホスト名/パス(例:
/realms/mcp)やプロキシ構成が変わるため、クライアント側でハードコードせず「どこに認可/トークン/登録を投げるか」を毎回 Discovery から解決します。 -
GET /realms/mcp/.well-known/openid-configuration→ 200
→ OIDC Discovery を取得し、authorization_endpointやregistration_endpointなどを解決。
GET /realms/mcp/.well-known/openid-configuration HTTP/1.1
Host: localhost:9001
Origin: http://localhost:6274
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Access-Control-Allow-Origin: http://localhost:6274
{
"issuer": "http://localhost:9001/realms/mcp",
"authorization_endpoint": ".../protocol/openid-connect/auth",
"token_endpoint": ".../protocol/openid-connect/token",
"registration_endpoint": ".../clients-registrations/openid-connect",
...
}
3. MCP サーバーが返す認可サーバーメタデータ
MCP サーバー自身の /.well-known/oauth-authorization-server も取得され、ここで Keycloak のエンドポイントが示される(issuer は Keycloak)。
GET /.well-known/oauth-authorization-server HTTP/1.1
Host: localhost:3001
Origin: http://localhost:6274
HTTP/1.1 200 OK
{
"issuer": "http://localhost:9001/realms/mcp",
"authorization_endpoint": "http://localhost:9001/protocol/openid-connect/auth",
"token_endpoint": "http://localhost:9001/protocol/openid-connect/token",
"registration_endpoint": "http://localhost:9001/clients-registrations/openid-connect",
"response_types_supported": ["code"],
"code_challenge_methods_supported": ["S256"],
...
}
4. Dynamic Client Registration(DCR)
-
POST /realms/mcp/clients-registrations/openid-connect→ 201 Created- Body:
redirect_uris=["http://localhost:6274/oauth/callback/debug"] -
token_endpoint_auth_method: none(パブリッククライアント) - 応答に
client_idとregistration_access_tokenが付与される。
- Body:
補足: DCR で何をしているか
- 事前に Keycloak へクライアント定義を用意せず、その場で一時的なクライアント(client_id)を発行する。
-
token_endpoint_auth_method: noneによりパブリッククライアントとして登録し、PKCE(code_challenge/code_verifier)で保護する前提。 - 応答に含まれる
registration_access_tokenで、このクライアント設定を後から更新/削除できる。 - ここで発行された
client_idを後続の認可リクエスト(/auth)やトークンエンドポイントで使用する。
ポイント:
- Nginx で CORS ヘッダを付け、
Access-Control-Allow-Origin: http://localhost:6274が返っているのがポイント。 - はまりポイント(MCP Inspector × Keycloak): Keycloak の DCR レスポンスはデフォルトで CORS が付かない。ブラウザ(Inspector)はクロスオリジンの
POST /clients-registrations/openid-connectを拒否するため、間に置く Nginx などのプロキシでAccess-Control-Allow-Origin: http://localhost:6274を明示的に付与する必要がある。Nginx を噛ませる理由は「CORS を後付けするための中継役」と捉えると分かりやすい。
注釈:
- パブリッククライアントとは: クライアントシークレットを安全に保持できない(ブラウザ/モバイル等)前提のクライアント。
client_secretを使わず、PKCE やリダイレクトURI検証で保護する。
リクエスト
POST /realms/mcp/clients-registrations/openid-connect HTTP/1.1
Host: localhost:9001
Content-Type: application/json
Origin: http://localhost:6274
{
"redirect_uris": ["http://localhost:6274/oauth/callback/debug"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code","refresh_token"],
"response_types": ["code"],
"client_name": "MCP Inspector",
"client_uri": "https://github.com/modelcontextprotocol/inspector",
"scope": "profile"
}
レスポンス
HTTP/1.1 201 Created
Location: http://localhost:9001/realms/mcp/clients-registrations/openid-connect/<client-id>
Access-Control-Allow-Origin: http://localhost:6274
{
"client_id": "<client-id>",
"registration_access_token": "<registration-access-token>",
"redirect_uris": ["http://localhost:6274/oauth/callback/debug"],
"response_types": ["code","none"],
"post_logout_redirect_uris": ["http://localhost:6274/oauth/callback/debug"],
...
}
5. 認可リクエスト(Authorization Code Flow)
-
GET /realms/mcp/protocol/openid-connect/auth?...→ 302 / 200- パラメータ:
response_type=code,client_id=<DCRで発行>,code_challenge,redirect_uri=http://localhost:6274/oauth/callback/debug,scope=profile,resource=http://localhost:3001/ - 302 で
login-actions/required-actionにリダイレクトされ、同意画面(Grant Access to MCP Inspector)が 200 で表示される。 - Cookie (
AUTH_SESSION_ID,KC_AUTH_SESSION_HASH等) がセットされ、以後のログイン/同意フローに使われる。
- パラメータ:
補足: 認可リクエストで何をしているか
- ブラウザから認可サーバー(Keycloak)の
/authにリダイレクトし、Authorization Code Flow を開始する。 -
code_challenge/code_challenge_method=S256で PKCE を利用し、後続のトークン交換時にcode_verifierを突き合わせる。 -
resource=http://localhost:3001/でアクセスしたいリソース(MCP サーバー)を指定し、発行されるトークンを紐付ける。 -
scope=profileで要求するスコープを指定。同意画面に反映される。 - ここではまだトークンは発行されず、同意画面を経て認可コードを受け取るためのステップ。
リクエスト
GET /realms/mcp/protocol/openid-connect/auth
?response_type=code
&client_id=<client-id>
&code_challenge=<code-challenge>
&code_challenge_method=S256
&redirect_uri=http%3A%2F%2Flocalhost%3A6274%2Foauth%2Fcallback%2Fdebug
&state=<state>
&scope=profile
&resource=http%3A%2F%2Flocalhost%3A3001%2F HTTP/1.1
Host: localhost:9001
Origin: http://localhost:6274
Cookie: AUTH_SESSION_ID=<cookie>; KC_AUTH_SESSION_HASH=<hash>; ...
302 レスポンス(抜粋)
HTTP/1.1 302 Found
Set-Cookie: AUTH_SESSION_ID=<cookie>; ...
Location: http://localhost:9001/realms/mcp/login-actions/required-action?...
Access-Control-Allow-Credentials: true
同意画面取得
GET /realms/mcp/login-actions/required-action
?execution=OAUTH_GRANT
&client_id=<client-id>
&tab_id=<tab-id>
&client_data=<client-data> HTTP/1.1
Host: localhost:9001
Cookie: AUTH_SESSION_ID=<cookie>; KC_AUTH_SESSION_HASH=<hash>; ...
HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
6. 同意フォーム送信(accept/cancel)
同意画面のフォームは login-actions/consent に POST され、hidden code と各種クエリを含みます。
POST /realms/mcp/login-actions/consent
?client_id=<client-id>
&tab_id=<tab-id>
&client_data=<client-data> HTTP/1.1
Host: localhost:9001
Content-Type: application/x-www-form-urlencoded
Cookie: AUTH_SESSION_ID=<cookie>; KC_AUTH_SESSION_HASH=<hash>; ...
code=<authorization-code-from-form>
accept=Yes # 「Yes」ボタンを押した場合。キャンセルなら cancel=No
レスポンス(ログではフォーム送信後のリダイレクト/トークン交換は未取得)
HTTP/1.1 302 Found
Location: <token 取得や redirect_uri への遷移先>
Set-Cookie: AUTH_SESSION_ID=<cookie>; ...
7. トークンエンドポイントでの認証情報取得(Authorization Code → Token)
同意後、Inspector がコードを用いてトークンを取得します(PKCE 使用)。client_secret は使わず code_verifier を送信。
POST /realms/mcp/protocol/openid-connect/token HTTP/1.1
Host: localhost:9001
Content-Type: application/x-www-form-urlencoded
Origin: http://localhost:6274
grant_type=authorization_code
&code=<authorization-code>
&code_verifier=<code-verifier>
&redirect_uri=http%3A%2F%2Flocalhost%3A6274%2Foauth%2Fcallback%2Fdebug
&resource=http%3A%2F%2Flocalhost%3A3001%2F
&client_id=<client-id>
レスポンス(アクセストークン/IDトークン等はマスク)
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"access_token": "<access-token>",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "<refresh-token>",
"token_type": "Bearer",
"id_token": "<id-token>",
"session_state": "<session-state>",
"scope": "profile"
}
ここから読み取れること・ハマりポイント
今回の環境/構成に限った話ではあるのですが、
-
メタデータのパス: Keycloak の OIDC Discovery は
/realms/<realm>/.well-known/openid-configuration。プレフィックスを間違えると 404 を踏む。 -
DCR の成功条件: Trusted Hosts 設定が通っているので 201 が返っている。もし 403 (
insufficient_scope / Host not trusted) が出る場合は、Trusted Hosts のホスト/IP/ポートを再確認する。 -
リソース指定: 認可リクエストで
resource=http://localhost:3001/を送っており、MCP リソースへのアクセスをトークンに紐付ける動きが見える。
まとめ
httplogs_noAuth.log の時系列を見ると、
- MCP 側メタデータ取得 →
- Keycloak の OIDC Discovery →
- DCR で一時的なクライアントを発行 →
- Authorization Code Flow に入り、同意画面まで到達
という流れが確認できました。エラーは「誤った Discovery パス」と「CORS/Trusted Hosts 周り」が主な落とし穴で、正しいパスとヘッダを揃えれば 201/302/200 のハッピーケースに乗ることがわかります。