Skip to content

Issuer ​

issuer(発行者)は OP の identity URL です。同じ文字列が、次のいくつもの場所に同時に現れます。

  • OP が署名するすべてのトークンの iss claim
  • /.well-known/openid-configuration の issuer フィールド
  • JWKS メタデータの帰属
  • プロファイルによっては、confidential client が署名する client_assertion の aud

トークン・署名鍵・discovery 文書を「同じ OP に属する」と紐付ける、ただひとつの文字列です。

30 秒で頭に入れる相関図

  • issuer は URL であって、ホスト名ではありません。https://login.example.com が issuer であり、login.example.com は issuer ではありません。
  • RP は byte 単位で完全一致 で比較します。厳密な RP にとって https://login.example.com と https://login.example.com/ は別物です。
  • 本ライブラリは正規形でない値を黙って正規化せず、起動を拒否します。op.WithIssuer に渡した値が、すべての発行物が使う形になります。
このページで触れる仕様

なぜ正規形が重要か ​

OIDC Discovery 1.0 §4 は、issuer 末尾のスラッシュを除いたうえで、host と path のあいだに /.well-known/openid-configuration を挿入して設定 URL を導出します。RFC 9207 はさらに、認可応答に同じ iss をエコーすることで「混同(mix-up)攻撃」を防ぐよう要求します — 誤って配送された code が別の AS に対して使われないようにするためです。どちらのチェックも、OP と全 RP が ひとつの 正規表記で合意していることに依存しています。

実際のデプロイでは、よく似ているが等価でない URI が並ぶことが多いです。

揺れ例何が壊れるか
末尾スラッシュhttps://idp.example.com/ vs https://idp.example.comDiscovery は URL の導出時に末尾のスラッシュを除きますが、issuer の文字列としては別物です。本ライブラリは後者だけを受け付けます。
デフォルトポートhttps://idp.example.com:443 vs https://idp.example.comOP がどちらか一方の形で iss を発行し、ロードバランサがもう一方に書き換えると、RP の byte 単位比較は破綻します。
scheme の大文字混在HTTPS://idp.example.comURL parser によっては parse 中に scheme を小文字化するもの・しないものがあり、生文字列を比較する RP と parse 後を比較する RP では別の issuer に見えてしまいます。
host の大文字混在https://Idp.Example.comホスト名の解決では大文字小文字を区別しませんが、コンポーネントごとの正規化が異なると issuer 文字列の完全一致に失敗します。
非正規 pathhttps://idp.example.com//oidc や .../a/../bコンポーネントによって path の正規化が異なると issuer 文字列が食い違います。本ライブラリはこれらの形を拒否します。
fragmenthttps://idp.example.com#mainfragment は HTTP リクエストのルーティングに関与しません。iss に持たせる構造的意味がありません。
query stringhttps://idp.example.com?env=proddiscovery URL の連結が壊れた形になります。仕様で禁止されています。

表記の食い違いは RFC 9207 などの issuer 完全一致チェックを失敗させますが、それだけで JWT の署名が無効になるわけではありません。本番トラフィックが CDN や別ホスト名経由で OP に届くまで気付かれない、典型的な失敗パターンです。

本ライブラリが起動時に拒否する形 ​

op.New は構築時の検証パスで issuer を検査し、不正な値のまま起動するのではなくエラーを返します。現時点で施行されているルール:

  • 空でない、parse 可能な URL であること。空文字列、url.Parse が失敗する文字列は拒否されます。
  • 絶対 URL で、authority を持つこと。oidc.example.com(scheme なし)も https:///path(host なし)も失敗します。
  • query を持たないこと。? を含む URL は拒否されます — discovery URL の連結が壊れるためです。
  • fragment を持たないこと。# を含む URL は同じ理由で拒否されます。
  • path に末尾スラッシュを持たないこと。https://idp.example.com/ も https://idp.example.com/oidc/ も拒否されます。issuer の path は discovery URL に埋め込まれるため、末尾スラッシュがあると同じ文書に対して非正規のもうひとつの綴りが生まれてしまいます。
  • scheme は全て小文字であること。HTTPS://idp.example.com も HtTpS://idp.example.com も拒否されます。検証は parse 後ではなく生入力に対しても行うので、parse 中に scheme を小文字化するパーサで揺れが見えなくなることはありません。
  • host は全て小文字であること。https://IDP.example.com も https://IDP.EXAMPLE.COM/oidc も拒否されます。u.Host は生の case を保持するので、authority の中に大文字があれば検査で落ちます。
  • デフォルトポートを省略すること。https://idp.example.com:443、https://idp.example.com:443/oidc、http://127.0.0.1:80 は拒否されます。冗長なデフォルトポートは、ロードバランサが片側だけ正規化した瞬間に byte 一致を壊します。
  • path が正規形であること。.. セグメント、. セグメント、重複スラッシュ(https://idp.example.com/a/../b、https://idp.example.com/a/./b、https://idp.example.com//oidc)は拒否されます。検証は path.Clean を通し、round-trip しないものは弾きます。
  • https であること。唯一の例外は loopback の IP リテラル(127.0.0.0/8 と [::1])で、開発時の起動が TLS なしで済むよう plain http を許容します。テキストの localhost は 既定の例外には含まれません。これは開発環境向けのライブラリポリシーです。RFC 8252 §7.3 はネイティブアプリのリダイレクト URI を扱い、issuer URL の例外を定めていません。明示的に許可するには op.WithAllowLocalhostLoopback() を使います。ローカルで passkey を扱う場合はこれが必要です。WebAuthn の Relying Party ID はドメインでなければならず、127.0.0.1 の issuer には対にできる RP ID が存在しないからです。rebinding の懸念が消えるわけではないので、この opt-in は開発マシンの中だけのものです。この例外があるため、issuer の検証は WithIssuer の中ではなく op.New の検証パスで走ります。オプションをどの順で渡しても例外が効くようにするためです。
go
// NG: host の大文字、default port、末尾スラッシュが byte 一致を壊す
op.WithIssuer("https://IDP.example.com:443/oidc/")

// OK: 正規形を 1 つに固定する
op.WithIssuer("https://idp.example.com/oidc")

これらのルールをまとめて適用することで、discovery 文書、発行する全トークン、認可応答の iss パラメタにわたって iss が byte 完全一致になることを保証します — RFC 9207 のミックスアップ防御で RP が比較するのはこの形です。

これらをすべて通過した値が正規形として採用されます。渡した値が iss の値、discovery 文書内の issuer フィールド、client_assertion の aud については、本ライブラリが受け付ける値の一つとして使われます。現行の FAPI 2.0 Security Profile §5.3.2.1(8) が認めるのは issuer 識別子の文字列だけですが、ライブラリはトークンエンドポイント URL や配列も受け付けます。そのため、設定だけで Final 仕様への適合は示せません。

設定方法 ​

go
provider, err := op.New(
    op.WithIssuer("https://login.example.com"),
    // ... 他のオプション
)

マルチテナントのデプロイで subpath を使うのも問題ありません。issuer の path は単なるメタデータではなく公開エンドポイントの名前空間の一部なので、op.New はすべてのプロトコルハンドラをその下に mount します。issuer が https://login.example.com/tenant-a なら、トークンエンドポイントは /tenant-a/oidc/token になり、他のエンドポイントもこれに揃います。

例外は discovery 文書だけで、ここは逆に取り違えやすい箇所です。OpenID Connect Discovery 1.0 §4 は、well-known の suffix を issuer の path の後ろではなく、host と path のあいだに挿入すると規定しています:

URL
issuerhttps://login.example.com/tenant-a
discovery 文書https://login.example.com/.well-known/openid-configuration/tenant-a
トークンエンドポイントhttps://login.example.com/tenant-a/oidc/token

本ライブラリは mount する URL と advertise する URL を一致させるので、discovery をたどる RP がこの違いに気付くことはありません。気付く必要があるのは、proxy のルールを手で書く運用者です。末尾スラッシュのルールは subpath にも適用されます: https://login.example.com/tenant-a は正規形、https://login.example.com/tenant-a/ は正規形ではありません。

デプロイの一生に渡って 1 つの値

RP がいったん issuer 文字列を保存したら、それを変えるのは破壊的変更です。最初の RP が連携する前に正規形を決めてください — 引退させずに済むホスト名を選ぶ、subpath の形を決める、ロードバランサに途中で host や scheme を書き換えさせない。本ライブラリは起動時の典型的なミスのほとんどを捕まえますが、TLS 終端しているプロキシが下流で path を削ったり host を小文字化していたりするのまでは捕まえられません。

次に読む ​