ユースケース — MCP 認可サーバ
MCP の認可仕様は、クライアントが事前に登録するのではなく、HTTPS URL で自身を示すことを推奨しています。その URL が client_id となり、クライアントが公開する JSON のメタデータ文書を指します(OAuth Client ID Metadata Document)。OP は文書を取得して検証し、キャッシュしている間だけ一時的なクライアントとして扱います。何も保存しません。
op.WithClientIDMetadataDocuments を指定すると、すべてのエンドポイント(認可、PAR、トークン、イントロスペクション、失効)でこの仕組みが有効になります。既定では無効で、指定しなければ OP の動作は従来と変わりません。
MCP クライアントのように、事前には把握できないソフトウェアにサインインさせたい場合に使います。クライアントをすべて自分で登録する場合は不要で、static クライアントや動的クライアント登録のほうが把握しやすくなります。
Experimental — IETF draft に追従
Client ID Metadata Document は公開 RFC ではなく IETF draft で、MCP 仕様が引用しているのは、ここで実装している版より前の版です。op.WithClientIDMetadataDocuments と関連するシンボルは godoc で Experimental: と明記しており、draft の更新で通信路上の形が非互換に変わる可能性があります。
組み込み方
import "github.com/libraz/go-oidc-provider/op"
op.New(
op.WithIssuer("https://op.example.com"),
/* required options */
op.WithProtectedResources(op.ProtectedResource{
Resource: "https://op.example.com/mcp",
ScopesSupported: []string{"mcp.read", "mcp.write"},
}),
op.WithClientIDMetadataDocuments(op.ClientIDMetadataDocuments{
Allow: func(ctx context.Context, clientID string) bool {
return strings.HasPrefix(clientID, "https://clients.example.org/")
},
}),
)有効にすると、discovery に client_id_metadata_document_supported: true が載り、token_endpoint_auth_methods_supported に none が含まれます。profile が none を外している構成では op.New が拒否し、FAPI profile との併用も拒否します。自己申告のクライアントは FAPI のクライアント要件を満たせないためです。
Allow は省略できます。指定すると、取得の前に、キャッシュヒットの場合も含めて毎回 client_id URL を渡して呼ばれ、false を返すとそのクライアントは未知になります。許可リストを狭めると即座に効きます。nil の場合は、URL の規則を満たすすべての URL を受け付けます。
対応するトポロジ: resource を同じ OP で公開する
MCP クライアントは、MCP サーバのアドレスを RFC 8707 の resource として送ります。対応している構成は、上の例のように OP が op.WithProtectedResources でその resource を公開する形です。メタデータ文書で識別されたクライアントは、そこで宣言した resource のどれでも要求でき、絞り込みはユーザの同意に委ねられます。別のホストにある MCP サーバが自前で resource メタデータを公開する構成は、このリリースの対象外です。
クライアントとして認められるもの
| 項目 | 受け付ける内容 |
|---|---|
token_endpoint_auth_method | none(既定)または private_key_jwt。ただし profile が許す方式の範囲内 |
grant_types | authorization_code と refresh_token |
response_types | code |
| PKCE | 必須(private_key_jwt でも) |
redirect_uris | 必須。動的クライアント登録と同じ規則で検証し、localhost の loopback URI だけは受け付ける |
client_secret* メンバー | 拒否 |
| 未知のメンバー | 無視 |
規則に反する文書は、全体を拒否します。private_key_jwt では jwks か jwks_uri が必須で、jwks に秘密鍵の成分を含めることはできません。文書の scope は、OP が公開している scope との積を取り、scope の無い文書にはその全部が与えられます。
同じ ID で登録済みのクライアント(static または動的登録)があれば常にそちらが優先され、取得は行いません。クライアントクレデンシャル、デバイス、CIBA、token exchange を使うクライアントは、URL で識別できません。
ユーザに見えるもの
URL で識別されるクライアントには first-party の consent 省略が適用されず、そのような URL を op.WithFirstPartyClients に入れると構築時に失敗します。consent 画面には、client_id URL のホスト(ポートが明示されていればポート付き)と、要求された redirect_uri のホストを表示します。redirect が loopback アドレスの場合は、警告も表示します。クライアントのロゴは取得も表示もしません。
独自の consent 画面は、同じ値を interaction.ClientView の MetadataDocumentHost、RedirectHost、RedirectLoopback で受け取るので、同様に表示してください。consent UI のカスタマイズを参照してください。
取得とキャッシュの上限
次の値は固定で、変更できません。
| 項目 | 値 |
|---|---|
| URL | https、/ 以外の path、query・fragment・userinfo・dot segment なし、ホストは ASCII、255 バイト以内 |
| 応答 | 200 のみ、application/json または application/*+json、リダイレクトなし |
| 本文 | 5 KiB |
| タイムアウト | 5 秒 |
| ネットワーク | private、loopback、link-local、メタデータのアドレスは接続時に拒否 |
| キャッシュ | Cache-Control の max-age を 60 秒から 24 時間に丸める。no-store は 60 秒扱い |
| 失敗 | 使えない文書は 5 秒だけ記憶する。gate が埋まっているときの失敗は記憶しない |
同じ URL への同時要求は 1 回の取得にまとめ、取得は JAR やクライアント JWKS の取得とプロセス全体の外向き gate を共有します。取得に失敗するとクライアントからは未知のクライアントに見え、理由(status、network、too_large、content_type、json、validation)は audit に cimd.fetch_failed として出ます。gate が埋まっているときは server_error で失敗します。
文書が変わった場合、新しい内容は次の取得から効きます。既存の grant とリフレッシュトークンは失効しません(理由)。
ローカル開発
op.WithAllowPrivateNetwork(op.OutboundClientIDMetadata) は、client_id URL について private と loopback の宛先および http スキームを許可するので、テストで httptest から文書を配信できるほか、クライアントがプライベートネットワーク上にある構成でも使えます。Client ID Metadata Documents は認証されていない呼び出し元が URL を決められるため、op.New は SSRF の露出として警告を出します。有効にする場合は ClientIDMetadataDocuments.Allow で対象を絞ってください。文書内の jwks_uri は別の取得で、op.OutboundJWKS の管理下にあります。
次に読む
- Protected resource メタデータ — MCP クライアントが要求する resource の宣言。
- 動的クライアント登録 — 把握しているクライアント向けの登録経路。
- 設計判断 — 取得の枠組み、consent の表示、この機能の限界。