Skip to content

設計判断 ​

OP が触れる仕様には、MUST、SHOULD、MAY が幾重にも層をなしています。複数の仕様で文言が矛盾する場面や、ある仕様の文字どおりの読みが別の仕様と衝突する場面では、どちらをどう取るかを決める必要があります。本ページはその選択を記録したものです。ある挙動が偶然なのか、仕様間の折衷なのか、意図したセキュリティ方針なのかを確かめるときに使ってください。

設計判断の読み方
各設計判断は、関係する仕様文、2 つの文書が食い違う箇所、本ライブラリが採用した規則を示し、その規則がどこで実際に強制されるかまで述べます。仕様文MUST · SHOULD · MAY節番号を添えて引用する食い違う箇所同じリクエストに対して 2 つの文書が別の答えを与えている本ライブラリが採用した規則採る解釈を 1 つ決め、その理由を書き残す強制する場所op.New の構築時検証 · ストアの契約 · internal の実装
以下の各項目はこの形に揃えています。仕様、衝突、判断の順に並び、判断の最初の文が実際の規則です。規則を強制するコードは実装箇所の行に示します。判断と意見を分けるのは最後の行です。どこかで具体的に強制されていない規則は規則ではなく、作者がこう考えたという注記にすぎません。
このページで使う用語
  • OP(OpenID Provider): 本ライブラリが実装する認可サーバ。
  • RP(Relying Party): OP を通じてユーザをサインインさせるクライアントアプリケーション。
  • RS(リソースサーバ): アクセストークンを受け付ける API。
  • grant: 1 人の subject が 1 つの client に与えた同意を、OP が記録したもの。1 つの grant の配下に、複数のセッションやトークンが並びます。同意 を参照してください。
  • chain: リフレッシュトークンと、ローテーションで生まれるその後継の連なり。リフレッシュトークン を参照してください。
  • tombstone: トークンごとの行を持たずに JWT を拒否するための、grant(または jti)をキーにした小さな失効レコード。

判断マップ ​

変更しようとしている領域のグループから読み始めてください。各行は規則を 1 文で示し、番号から本文の項目へ移れます。

プロファイルと discovery

トークン、grant、失効

登録、issuer、外向き fetch

ブラウザ、セッション、UI 境界

JOSE と request object

1. PAR の request_uri を消費する時点 ​

JOSE と request object

仕様 RFC 9126 §2.2 は、クライアントの利用と AS の扱いを分けています。

RFC 9126 §2.2(要約) クライアントは各 request_uri を 1 回だけ使わなければなりません。AS は one-time として扱うことが推奨されますが、ユーザエージェントの再読み込み・更新による重複要求を許容してもかまわず、期限切れの値は拒否しなければなりません。

衝突 「最初の参照で消費する」という文字どおりの解釈は、複数段階の interaction で壊れます。同意 → MFA → ロケール変更の途中で、同じ /auth?request_uri=… へ再リダイレクトすることがあるためです。

OFCS の FAPI 2.0 Security Profile Final plan は両側を検証します。…-par-ensure-reused-request-uri-prior-to-auth-completion-succeeds は認証完了前の再アクセスの 受理 を、…-par-attempt-reuse-request_uri は認証完了後の再アクセスの 拒否 を要求します。

判断

request_uri は /auth の入口で参照し(PARs.Find)、認可コード発行時点で消費します(PARs.Consume)。

interaction 中の再アクセスは成功し、コード発行後の再アクセスは invalid_request_uri を返します。

OFCS module fapi2-…-par-attempt-reuse-request_uri がコード発行後の拒否経路を検証します。詳細は OFCS 適合状況 を参照してください。

2. リフレッシュのローテーション猶予期間 ​

トークン、grant、失効

仕様 RFC 9700 §4.14.2 は、公開クライアントについて sender-constrained refresh token またはローテーションで replay を検出するよう AS に求めています。FAPI 2.0 Security Profile Final §5.3.2.1(9) は、refresh token のローテーションを例外的な状況に限り、新しい token を失った場合は旧 token による時間制限付き retry を許容しています。

RFC 9700 §4.14.2(要約) ローテーションでは新しい refresh token を発行して旧 token を無効化し、関係を保持します。再利用で侵害が分かると有効な token を失効させます。

衝突 「厳密な single-use、猶予なし」の解釈では、ローテーションのネットワーク往復が中断したクライアント(モバイルのバックグラウンド遷移、TCP 切断、HTTP/2 ストリームのリトライなど)がすべて動かなくなります。

OFCS の FAPI 2.0 Final plan はリフレッシュトークンをローテーションしない FAPI プロファイルに対して実行するので、…-refresh-token モジュールは猶予期間を検証しません。猶予期間が効くのは、ローテーションするプロファイルです。

判断

ローテーション済みの refresh token は、ライブラリで設定できる GraceTTL(既定 60 秒)の間、chain が replay 検知で失効していない限り受理されます。

  • 猶予期間内に旧トークンを提示すると、chain を再度ローテーションせずに保存済みの後継トークンを返します。送信者制約付きの再試行では、新たにバインドしたアクセストークンを発行する場合があります。
  • 猶予期間 を過ぎたあと の再利用、あるいはすでに失効した chain での再利用は、chain 全体を無効化し、その grant のアクセストークンも失効させます(RFC 9700 §4.14.2)。

既定の 60 秒は、応答を失った場合の限定的な retry 経路に対する実装上の選択であり、RFC の要求ではありません。op.WithRefreshGracePeriod は負値を拒否し、明示した 0 は猶予なしとして扱います。それ以外は、負でない任意の時間幅を選べます。

ローテーションが触れるのは、その chain だけです。後継トークンは置き換えるトークンの scope を引き継ぐため、refresh リクエストが 1 回分のアクセストークンのために scope を絞っても、後継は絞られません。ローテーションが失効させる opaque アクセストークンは同じ chain から発行されたものに限るので、同じ client 上のユーザの他のセッションのトークンは残ります。store.RefreshToken.Revoked が「ローテーションで消費済み」と「chain 侵害で失効」を区別し、store.RefreshToken.ChainID が chain を識別します。

2 つの FAPI 2.0 profile は、FAPI 2.0 Security Profile Final §5.3.2.1(9) に従ってリフレッシュトークンをローテーションしないため、猶予期間を持ちません。リフレッシュ応答に新しいリフレッシュトークンは含まれず、提示したトークンは期限が切れるか失効するまで使えます。この option は、これらの profile では op.New が拒否します。この判断の残りは、ローテーションする profile にだけ当てはまります。

実装箇所 internal/grants/refresh.Exchanger.tryGrace

3. offline_access は判定ではなくシグナル ​

トークン、grant、失効

仕様 OIDC Core 1.0 §11 は、次の 2 つを同時に書いています。

OIDC Core 1.0 §11 (a) 訳 リフレッシュトークンの利用は Offline Access 専用ではない。

OIDC Core 1.0 §11 (b) 訳 offline_access 要求時は、AS は prompt パラメータに consent を含めなければならない。

衝突 (a) を取れば、refresh_token grant が許可されたクライアントなら scope に依らずリフレッシュトークンを発行する、と読めます。(b) を取れば、offline_access こそリフレッシュ発行のための scope であり、同意の判定はその副次だと読めます。OP の実装ごとに解釈が割れる箇所です。

判断

既定では、offline_access は UX と TTL のシグナルであり、リフレッシュトークン発行の判定には使いません。

リフレッシュトークンは、付与 scope に openid が含まれ、かつクライアントの GrantTypes に refresh_token が含まれる場合に発行されます。grant が offline_access も持つ場合は、op.WithRefreshTokenOfflineTTL によってその chain を offline 用の寿命枠に入れられます。同意・監査の表示も、ユーザが認可した offline の到達範囲を反映できます。

OIDC Core §11 を厳格に読みたい組み込み側は、op.WithStrictOfflineAccess() を渡します。この mode では次のようになります。

  • リフレッシュトークンの発行に、offline_access が追加で必要になる
  • grant_type=refresh_token でも、元の grant が offline_access を持たない token は invalid_grant で拒否される

この option は op.WithOpenIDScopeOptional() と併用できません。OIDC request ではないものに §11 を適用しても意味をなさないためです。

実装箇所 op/options_features.go、internal/tokenendpoint/strict_offline_test.go、refresh 発行経路

4. ループバック redirect のポート ​

登録、issuer、外向き fetch

仕様 RFC 6749 §3.1.2.3 と OAuth 2.1 は、redirect_uri のバイト完全一致を要求しています。一方で RFC 8252 §7.3 は、ネイティブアプリのループバック redirect について「リクエスト時に任意のポートを許可しなければならない」としています。

衝突 構造的な矛盾です。CLI ツールは OS が割り当てる動的ポートを事前登録できないため、完全一致を厳密に適用するとネイティブアプリの標準的なフローが壊れます。

判断

既定は完全一致(OAuth 2.1 を厳格に解釈)です。RFC 8252 §7.3 の緩和はクライアント単位のオプトインとし、登録済みの redirect_uris にループバック URI が含まれる場合に限って有効になります。

ポート不一致を許容するのは、次の 4 条件がすべて成り立つときだけです。

  • scheme が http である
  • 登録済みの host がループバック形(127.0.0.1、::1、文字列の localhost)のいずれかである
  • 要求側の host が登録済みの host と一致する
  • path、query、fragment が完全一致する

文字列の localhost は、authorize 側の判定を OIDC Registration の native client 向け loopback の例外規定と揃えるために受理します。これがないと、http://localhost/cb で登録した native アプリは、登録は通るのに、OS から動的ポートを受け取った時点で /auth で弾かれます。

この localhost の受理は、組み込み側が登録時にオプトインしている場合にだけ意味を持ちます。web クライアントは op.WithAllowLocalhostRedirectURIs()、native クライアントは application_type=native です。DNS rebinding を懸念するデプロイは、両方のオプトインを外しておけば、literal IP のみの厳格な方針を保てます。

HTTPS ループバックは緩和の対象外です(127.0.0.1 向けの ACME 証明書を発行する経路がないため)。

5. iframe 前提のログアウト仕様 ​

ブラウザ、セッション、UI 境界

仕様 Session Management 1.0 と Front-Channel Logout 1.0 は、いずれも OIDC Core とは別に公開されている OpenID Connect の仕様です。

衝突 どちらも、サードパーティの iframe が埋め込まれた状態で自分自身の cookie を読めることを前提にしています。モダンブラウザの既定(2017 年以降の Safari ITP、2019 年以降の Firefox ETP、2020 年以降の Chrome SameSite=Lax、2024 〜 2025 年のサードパーティ cookie 段階廃止)が、この前提を取り去りました。

判断

未実装です。

discovery 文書には frontchannel_logout_supported も check_session_iframe も出力しません。代わりに RP-Initiated Logout 1.0 + Back-Channel Logout 1.0(署名済みの logout_token を server-to-server で POST する形式)を提供しています。

iframe ベースのセッション通知が必要なら、別ライブラリを選んでください。これは未着手の TODO ではなく、設計上のスコープ判断です。

6. JAR の request object の replay ​

JOSE と request object

仕様 RFC 9101 §10.8 は、exp 期限内の request object の replay を拒否することを推奨(RECOMMENDED)しています。レジストリの実装そのものは必須ではありません。

衝突 署名を検証して受理するだけでは、JAR の最低限の規則は満たせても、傍受された request object を exp まで繰り返し /auth に提示できます。FAPI 2.0 Message Signing は PAR で署名付き request object を求めますが、jti による再利用検知を必須にはしていません。以下のキャッシュはライブラリ独自の追加防御です。

判断

jti を持つ request object は重複排除します。

OP は jar:<use>:<clientID>:<jti> を ConsumedJTIStore に記録し、verifier の max-age / max-lifetime に clock skew を足した時間幅のあいだ保持します。保持期間は OP 側の上限で切り詰めるため、client が遠い未来の exp を指定しても marker を無期限に占有できません。ストアは DPoP 証明や client assertion が使うものと同じなので、複数インスタンスで共有すれば 3 つの防御がそろいます。

jti がない場合は次のとおりです。

  • /auth と /par では受理します。RFC 9101 §6.1 がこの claim を任意としており、OFCS の FAPI message signing plan も jti のない request object を送るためです。
  • /bc-authorize では、FAPI-CIBA が jti を必須に引き上げるため、欠けていれば拒否します(ErrJTIMissing)。

256 byte を超える jti は不正な request object として扱います。

7. プロファイルが有効化するもの ​

プロファイルと discovery

仕様 FAPI profile の要件には、PAR / JAR / JARM のように route へ追加できる feature と、デプロイ側が用意する collaborator を必要とするものがあります。RFC 9700 §2.1.1 は public client に PKCE を要求し、confidential client には推奨しています。本ライブラリは既定の profile.OAuth21 と FAPI profile で、これより厳しい「すべての client に適用する」ルールを採用します。互換性が必要な配備では、profile.OIDCCore を宣言して OIDC Core の形を残せます。

衝突 ハンドラごとに switch profile を書くと、知らぬ間に挙動がずれていきます。すべての要件を自動有効化するのも誤りです。feature は安全に選べますが、store や resolver が設定されていない grant を mount すれば、endpoint だけが存在する中途半端な構成になります。

判断

profile はポリシーを重ねますが、feature と grant の扱いを分けます。WithProfile は必要な feature を自動有効化し、必要な grant は決して自動有効化しません。

feature:

  • FAPI 2.0 Baseline は PAR と JAR を選び、Message Signing はさらに JARM を選びます。
  • FAPI profile は、組み込み側が mTLS を明示しない限り DPoP を選びます。
  • profile.OAuth21 は route に関わる feature を追加しません。

PKCE: profile.OAuth21、profile.FAPI2Baseline、profile.FAPI2MessageSigning は、confidential client を含むすべての authorization-code request で PKCE を必須にします。WithProfile なしの OP は profile.OAuth21 で動きます。緩める場合は明示が必要です。profile.OIDCCore は confidential client の PKCE を任意のままにし、他のプロファイルとは組み合わせられません。そのため、オプションの書き忘れが副作用として弱い方針を生むことはありません。

grant: profile.FAPICIBA で CIBA grant がなければ op.New は失敗し、WithCIBAGrant(CIBAGrant{HintResolver: ...}) を設定経路として示します。collaborator のない /bc-authorize を mount する代わりに、構築時に意味のあるエラーを返すためです。

解決後の profile、feature、grant、policy は startup.profile audit event として 1 回出力されます。運用者は最初の request より前に、実際の方針を確認できます。

実装箇所 op/profile/constraints.go(制約関数の適用は WithProfile と構築時 validation に集約)

8. 通信路上の ACR と内部 AAL ​

プロファイルと discovery

仕様 OIDC Core 1.0 は acr claim を発行し、RFC 6711 / 8485 は識別子レジストリを規定しています。RFC 9470 は、acr_values と WWW-Authenticate: error="insufficient_user_authentication" によるステップアップを標準化しています。

衝突 内部 AAL(「ユーザがどの factor を完了したか」)と、通信路で RP に届く OIDC の acr は、別の語彙です。安直に対応づける(たとえば acr=urn:authn:aal=2 のようなリテラル)と、通信路上の語彙が内部分類に縛られます。組み込み側は、RP を壊さずにはその語彙を変えられなくなります。

判断

2 層モデルです。内部の AAL 階層と、通信路上の acr マッピングを分けて持ちます。

RuleACR が RFC 9470 のステップアップを実装しています。RP が現在のセッションより高い acr_values を要求すると、insufficient_user_authentication チャレンジを次のステップとともに返します。

実装箇所 op/aal.go(AAL 階層)、op/acr.go(acr マッピング)、op/rule.go(RuleACR)

9. issuer の正規化 ​

登録、issuer、外向き fetch

仕様 RFC 9207 は認可応答に iss を付けることを必須とし、クライアントはその値を期待する issuer と単純な文字列比較で照合します。OIDC Discovery 1.0 §3 と RFC 8414 §2 は issuer を query / fragment のない HTTPS URL と定義し、metadata の issuer が設定文書の取得に使った issuer と一致することを要求しています。Discovery §4.1(および RFC 8414 §3.1)は、well-known の URL を組み立てる前に path issuer の末尾 / を取り除きます。FAPI 2.0 Security Profile Final §5.3.2.1(1) は、その discovery metadata を AS が配布することを要求しています。

衝突 URI の構文上は、scheme や host の大文字小文字、既定ポートの表記、末尾 / が違う表現も許されます。Discovery も well-known URL の組み立て時に末尾 / を取り除きます。これらの仕様は、ライブラリがすべての表現を拒否することまでは要求していません。一方、RFC 9207 と metadata 仕様は、照合する境界ごとに 1 つの issuer 値を要求します。OP と RP の正規化が違えば、issuer 検証は失敗します。

判断

op.WithIssuer は意図的に厳格な正規化ポリシーを適用します。op.New は非正規 issuer では起動せず、ビルド時にエラーを返します。

次のいずれかを含む URI を拒否します。

  • 末尾のスラッシュ
  • scheme の大文字混在
  • host の大文字混在
  • 既定ポート(https は :443、http は :80)
  • fragment
  • query
  • 非正規 path(..、.、path.Clean で検出される重複スラッシュ)

query / fragment の拒否は issuer の仕様要件に従ったものです。大文字小文字、ポート、末尾のスラッシュ、path の規則は、ライブラリが追加したポリシーです。

受理した同じ形を、iss を出すすべての場面で使います。発行するすべてのトークン、discovery 文書の issuer フィールド、認可応答の iss パラメータです。そのため、RFC 9207 のミックスアップ防御で行う単純な文字列比較は、end-to-end で同じ値を比べます。

10. セッションの置き場所 ​

ブラウザ、セッション、UI 境界

仕様 該当なし。アーキテクチャ上の判断です。

衝突 セッションを認可コード / リフレッシュトークン / クライアントと同じ SQL クラスタ(トランザクションストア)に置くと、セッションの書き込みレイテンシがトランザクション経路に直結します。Redis に置くと耐久性が下がります。ただし、セッションを失ってもユーザが再ログインすれば回復できますが、認可コードを失うと RP のループが壊れます。

判断

セッションは別のサブストア(store.SessionStore)を通し、組み込み側は Redis などの揮発ストアに置くこともできます。

/end_session は削除前にセッションの snapshot を取得し、BCL coordinator は GrantStore.ListClientIDsBySubject による grant 起点の client ID から audience を解決します。snapshot の取得前にセッションが追い出されると、fan-out は始まらず、BCL の no-sessions event も出ません。

audit.BCLNoSessionsForSubject が意味するのは別の状況です。セッションを伴う logout 通知が coordinator に届いたものの、対象となる grant 起点の RP が 0 件だった場合を示します。

op.SessionDurabilityPosture は、組み込み側が宣言した耐久性を記録します。運用者はこの event を追い出しの直接のシグナルとみなさずに、logout の契機と snapshot 保持のリスクを解釈できます。

11. JOSE の許可リストと単一の署名アルゴリズム ​

JOSE と request object

仕様 RFC 8725(JWT BCP)§3.1 は、ライブラリが呼び出し側に許可アルゴリズム集合を指定させ、その集合以外を使わないよう求めています。§3.2 は採用アルゴリズムをアプリケーションが決めるものとし、HMAC 鍵と公開鍵の混同を警告しています。別の end-to-end 保護がある場合の none を一律には禁止しておらず、ライブラリが none を生成・消費するのは明示指定がある場合に限るべきだとしています。

衝突 基盤の JOSE ライブラリは、レジストリ全体を既定で受理します。実行時チェックを足すのは壊れやすく、将来 JOSE ライブラリを直接 import する経路ができればすり抜けてしまいます。

判断

none と HS* はアルゴリズムの型にそもそも存在しません。OP が署名するものはすべて、OP ごとに 1 つのアルゴリズム(既定は ES256、指定すれば RS256)を使います。

internal/jose.Algorithm は閉じた列挙型(RS256、PS256、ES256、EdDSA)です。depguard(lint)は、生のハンドルをすでに扱っている少数の internal/ パッケージ(jose、keys、dpop、jar、jarm、mtls、clientauth など)以外から JOSE パッケージを直接 import することを禁止しています。新しいコードが使うには、このリストへ意図して追加する必要があります。

検証側の許可リストと発行側の方針は、意図的に異なります。client assertion と request object は、許可された非対称アルゴリズムを使えます。OP が署名するもの — ID Token、JWT アクセストークン、署名付き UserInfo、JARM response、logout token、introspection JWT、ID-JAG、Security Event Token — は、OP の 1 つのアルゴリズムだけを使います。

  • 既定は P-256 鍵による ES256 です。
  • op.WithSigningAlgorithm(op.AlgRS256) を指定すると、2048 bit 以上の RSA 鍵による RS256 になります。
  • オプションが受け付ける値はこの 2 つだけです。オプションとアクティブ鍵が食い違うと、op.New が失敗します。

選択は OP 全体で 1 回です。クライアントごとの選択や交渉はありません。*_signed_response_alg のクライアントメタデータは OP のアルゴリズムと一致する必要があり(none を許す箇所では none でも可)、動的登録は id_token_signed_response_alg を OP のアルゴリズムで補います。RS256 は FAPI2Baseline、FAPI2MessageSigning、FAPICIBA と同時に指定できません。FAPI 2.0 Security Profile §5.4.1 が許可するのは PS256、ES256、EdDSA だけだからです。OP が発行するトークンに PS256 を求める RP は対象外です。

OP 自身の検証(アクセストークン、id_token_hint、token exchange の subject token)は、JWS ヘッダの alg が一致した鍵のアルゴリズムと等しいことを求めます(RFC 8725 §3.1)。発行を 1 つのアルゴリズムに絞ることで、標準どおりの client assertion を受け付けたまま、アルゴリズムの交渉と downgrade の経路をなくせます。

既存の OP を切り替える手順は 署名アルゴリズムを切り替える を参照してください。

実装箇所 internal/jose(Algorithm)、import の制限は depguard

12. プロファイルで絞り込む discovery ​

プロファイルと discovery

仕様 OIDC Discovery 1.0 と RFC 8414 がメタデータフィールドを列挙しています。FAPI 2.0 Security Profile Final §5.3.2.1(6) は AS の client authentication を mTLS または private_key_jwt に限定しており、その結果 token_endpoint_auth_methods_supported も絞られます。RFC 9101 §10.1 は、JAR 有効時に request_object_signing_alg_values_supported を要求しています。

衝突 _supported 系のリストは、feature の有効化に応じて作られ、有効プロファイルで絞り込まれ、その後 introspection / revocation の auth-method リストにコピーされます。これをハンドラで毎回やるとずれが生じます。構築時に行えば、1 か所で整合を取らざるを得なくなります。

判断

discovery は op.New 時に 1 度だけ構築します。

プロファイルは ProfileAllowedAuthMethods で auth-method リストを絞り、introspection / revocation は絞り込み後のリストをコピーします。

リポジトリ内の discovery golden test がプロファイルごとの文書構造を検証しているので、気付かれずに進行するずれも PR の段階で検出できます。

実装箇所 internal/discovery/build.go

13. client_assertion の audience ​

プロファイルと discovery

仕様 RFC 7523 §3 は、JWT bearer assertion の aud を「authorization server を識別する値」と要求するだけで、どの識別子かまでは固定していません。OIDC Core 1.0 §9(private_key_jwt)はこれを OP の token endpoint URL と読みます。FAPI 2.0 Security Profile Final §5.3.2.1(8) は認可サーバ側の要件を絞り、issuer identifier の値だけを JSON 文字列として受け入れるよう求めています。FAPI client 側にも §5.3.3.1(5) に同じ形の要件があります。RFC 7519 は一般には aud の文字列または配列を許しますが、FAPI はこの形を絞ります。

衝突 ライブラリの互換 verifier は、OIDC Core の token endpoint 形、FAPI の issuer 形、受理済みの値を含む配列を受け入れます。1 つの OP で両方の方言を扱うには便利ですが、token endpoint URL や aud 配列を受け入れることは §5.3.2.1(8) に反します。Final FAPI プロファイルでは、RFC 7523 の広い解釈よりもプロファイルの要件を優先し、より厳格な検証方針が必要です。

判断

2 つの FAPI 2.0 プロファイルでは、verifier は JSON 文字列の issuer identifier だけを受け入れます。それ以外のプロファイルでは、主 Audience と AuxAudiences を使う互換動作を維持します。

  • 互換動作のプロファイル: op.New はすべての OP で AuxAudiences に issuer を入れます。assertion の経路は統合後に jti を 1 回だけ消費するため、受理したどちらの方言でも replay 防御をすり抜けることはありません。
  • profile.FAPI2Baseline と profile.FAPI2MessageSigning: 同じ verifier が、token endpoint 形と配列形を §5.3.2.1(8) のとおり invalid_client で拒否します。プロファイルは 1 つの verifier の切り替えであり、別の verifier を並べているわけではありません。
  • profile.FAPICIBA は CIBA Core §7.1 に従い、広い集合を維持します。

実装箇所 internal/clientauth/assertion.go

14. PKCE は S256 のみ ​

JOSE と request object

仕様 RFC 7636 §4.2 は plain と S256 の 2 種を列挙しています。§4.4.1 は SHA-256 を計算できない環境のために plain を残しつつ、クライアントには S256 を推奨しています。OAuth 2.1 (draft-ietf-oauth-v2-1 §4.1.1) と FAPI 2.0 Security Profile Final §5.3.2.2(5) は、plain を完全に禁止しています。

衝突 RFC 7636 を素直に読めば、クライアントが要求した方式を OP は受理します。OAuth 2.1 / FAPI 2.0 を厳格に読めば拒否です。「プロファイルごとのゲート」という設計にすると、同じクライアントが OIDC Core の OP では成功し、同じ OP の FAPI 2.0 プロファイル下では失敗します。これは、コードベース全体で避けるように作っている「気付かれずに進行するプロファイル間のずれ」の典型例です。

判断

プロファイルに関係なく plain を拒否します。

internal/pkce.Method は "S256" の単一定数です。ValidateChallenge はそれ以外を ErrChallengeMethodUnsupported で返し、discovery の code_challenge_methods_supported には S256 だけを載せます。

理由は次の 3 点です。

  1. plain は PKCE の防御になりません(verifier がそのまま challenge になるため)。
  2. RFC 7636 の SHOULD という読みは、OAuth 2.1 によって時代遅れになっています。
  3. プロファイルをまたいでクライアントの挙動を一定に保てます。

実装箇所 internal/pkce(Method、ValidateChallenge)

15. DPoP リフレッシュのバインド — public と confidential ​

トークン、grant、失効

仕様 RFC 9449 §5.0 / §5.4 は、AS が token endpoint で受け取った DPoP 鍵にリフレッシュトークンをバインドしてもよい(MAY)としています。§5.4 は加えて、いったんバインドしたらローテーションをまたいでバインドを保持することを必須としています。

衝突 「MAY 束縛」は、どちらにも取れます。

  • 常に束縛 にすると、confidential クライアントは chain が存続している間ずっと 1 つの DPoP 鍵に固定されます。refresh のたびに DPoP 鍵をローテーションする FAPI 2.0 OFCS のプランと衝突します。
  • 常に未束縛 にすると、public クライアント(SPA、ネイティブアプリ)のリフレッシュトークンが保護のない bearer のまま残ります。これはまさに、RFC 9449 §1 が sender constraint を導入する動機として挙げる脅威モデルです。

判断

public は束縛し、confidential は未束縛にします。

  • TokenEndpointAuthMethod が "none"(public クライアントのシグナル)のクライアントは、初回発行時にリフレッシュ chain を DPoP 鍵にバインドします。§5.4 に従い、ローテーションをまたいでバインドを保ちます。
  • confidential クライアント(private_key_jwt、client_secret_*)は chain を未束縛にし、refresh ごとに DPoP 鍵をローテーションできるようにします。ただし、refresh のたびに発行されるアクセストークンは、提示された DPoP 鍵にバインドされます。アクセストークンを保持する側には、依然として対応する秘密鍵が必要です。

chain がいったんバインドされると §5.4 のバインド維持ルールが適用されるため、後付けで便宜的にアップグレードして鍵ローテーションをロックすることはできません。

この分割は DPoP だけの話です。mTLS にバインドされたリクエストで発行したリフレッシュ chain は、クライアント種別を問わずクライアント証明書にバインドされ、リフレッシュのたびに同じ証明書の提示が必要です(requireMTLSMatch)。

実装箇所 internal/tokenendpoint.refreshDPoPJKT

16. 誰が何を introspect できるか ​

トークン、grant、失効

仕様 RFC 7662 §2.2 は次のように規定しています。

RFC 7662 §2.2(訳) AS が active とみなさないトークンへの introspection 応答は "active": false を含まなければならず、その場合は active メンバだけを返してもよい。

§2.1 は、AS が「audience に応じて応答を変えてもよい」とも定めています。クロスクライアントの拒否は許容されますが、必須ではありません。

衝突 解釈は大きく 3 つあります。

  1. 緩い解釈 — AS が認識するすべてのトークンを、認証済みなら誰でも introspect できる。発行先でないクライアントにも "active": true を返す。
  2. 厳格 + 区別可能 — クロスクライアントの introspection は拒否するが、区別可能な形で通知する(HTTP 403 や error: not_authorized など)。
  3. audience 単位の委譲 — token の audience が示す resource server を明示的な allow-list で認め、それ以外の caller には unknown / expired / revoked と同じ {"active": false} 形を返す。

解釈 2 は、応答の形からトークンの存在を漏らします。応答の形の違いを観測すれば、推測したトークンが どこかの 有効な grant に属するかを調べられてしまいます。

判断

/introspect は confidential caller だけに開きます。別 client のアクセストークンの introspection は、token の audience を通してだけ委譲します。

  • public client、または token endpoint 認証方式が none の client は、token の解決より前に 401 invalid_client を受け取ります。
  • confidential client は、自分に発行されたアクセストークンとリフレッシュトークンを introspect できます。
  • 別 client のアクセストークンは、token の audience が示す protected resource の IntrospectionClients に caller が登録されている場合だけ introspect できます。この audience の確認が委譲の境界です。
  • リフレッシュトークンは、別 client へ委譲しません。
  • 認められない別 client のアクセストークン照会には、unknown / expired / revoked と同じ {"active": false} 形を返します。

active な opaque の応答には iss も含まれます。active なリフレッシュトークンの応答には token_type を含めません。RFC 7662 §2.2 がこのメンバーをアクセストークンの種別と定めているためです。

confidential の制限、audience による委譲、リフレッシュトークンの拒否はテストで固定しています。

実装箇所 internal/introspectendpoint、protected-resource 設定

17. /end_session のカスケード範囲 ​

トークン、grant、失効

仕様 OIDC RP-Initiated Logout 1.0 §6 は次のように規定しています。

OIDC RP-Initiated Logout 1.0 §6(訳) ユーザがサインアウトしたあとに、OP はアクティブなセッション、リフレッシュトークン、アクセストークンを失効させてもよい。

Back-Channel Logout 1.0 §2.3 は RP 側の fan-out を規定するだけで、アクセストークン失効の到達範囲には触れていません。

衝突 「MAY」を素直に読むと、「cookie だけ消して終わる」から「subject が保有する grant をすべて失効させる」まで幅広く取れます。影響範囲はそれぞれ違います。

  • cookie のみ: 発行済みのアクセストークンは exp まで有効なままです。OP に戻ってこない JWT は引き戻せません。
  • OP のエンドポイントで失効させる: /userinfo と /introspect はその場で inactive を返します。JWT をオフラインで検証する RS には届きません。
  • opaque トークン はあらゆる RS に届きます。オフライン検証の経路がないので、次の introspection で inactive になります。

判断

既定でカスケードします。/end_session は subject が保有する grant をすべて列挙し、それぞれの配下に発行されたものを失効させます。

  • JWT アクセストークン。設定した失効戦略に従います(既定は grant tombstone)。tombstone 戦略では、その grant への以後の発行も止めます。
  • opaque アクセストークン。
  • リフレッシュトークン。

各クラスは、設定した Store が公開するサブストアを通して失効させます。サブストアがないクラスは飛ばします。Grants を公開しない store では「cookie のみ」の方針になり、アクセストークンは自然失効を待ちます。失敗したクラスは audit に記録しますが、サインアウト自体は止めません。

他の失効契機との到達範囲の比較は #19 の表にあります。JWT と opaque で到達範囲が違う点(JWT は OP のエンドポイントのみ、opaque は introspection 経由ですべての RS まで)は、access-token-format ページに切り出してあります。この差が問題になる組み込み側は、どちらの形式を発行するかを選ぶことで対処できます。

18. アクセストークンの形式 ​

トークン、grant、失効

仕様 RFC 9068 は JWT 形式の OAuth 2.0 アクセストークンを標準化しています。RFC 6749 自体は、アクセストークンを opaque な bearer 文字列として扱うだけです。RFC 7662 の introspection は任意です。RFC 8707 は、単一のクライアントが 1 回のフローで複数の resource 向けトークンを要求できるようにしています。

衝突 どちらの方向の既定にしても、筋は通ります。

  • 既定 JWT — 各 RS がローカルで検証でき、/introspect の往復が不要で、水平にスケールできます。ただし、/end_session は OP に戻ってこない JWT を exp まで引き戻せません。
  • 既定 opaque — 各 RS が OP を呼び出すため、レイテンシが OP の経路と結合します。ただし、失効が即時に届きます。

ハードコードされた単一の既定は、すべての組み込み側を 1 つのトレードオフに縛ります。RFC 8707 の audience 単位の選択を採用すれば、同じ OP が同じ認可の中で、異なる RS に異なる形式を発行できます。

判断

既定は op.AccessTokenFormatJWT です。opaque はオプトインで、全体にも audience ごとにも指定できます。

JWT は典型的な水平スケールの RS 構成と一致し、通信路上の形式が既製の JWT verifier と互換になります。

  • op.WithAccessTokenFormat(op.AccessTokenFormatOpaque) で、全体の既定を opaque に切り替えます。
  • op.WithAccessTokenFormatPerAudience(map[string]op.AccessTokenFormat{...}) で、RFC 8707 の resource indicator 単位に選びます。失効の到達範囲を重視する audience(admin API、payment API など)は opaque、汎用 API は JWT、というふうに混在させられます。

トレードオフの全体(負荷の集中、ヘッダサイズ、カスケードの到達範囲、ストレージ形式)は access-token-format ページにまとめてあります。本項目は、選択 の事実を他の意図的な仕様の読みと並べて残すためのものです。

19. JWT アクセストークンの失効 ​

トークン、grant、失効

仕様 RFC 6749 §4.1.2 は、コード再利用時に AS が「可能であれば失効させるべき」としています。RFC 6819 §5.2.1.1 は、サーバ側に再利用検出の不変条件を期待しています。RFC 7009 §2.2 は、self-contained なトークンの revocation を未対応にしてもよいと許容しています。FAPI 2.0 Security Profile Final §5.3.4(3) は、RS にアクセストークンの有効性、完全性、期限、失効状態の検証を要求しています。この要件は RS の境界で課されるもので、OP 側の保存方式を規定してはいません。

衝突 「すべての jti を shadow し、revocation で行を反転する」という素直なモデルは、すべての仕様を満たします。ただし、トランザクションストアに O(発行レート × AT_TTL) の行を載せることになり、N 件の AT を持つユーザのログアウトは N 件の UPDATE になります。逆に「失効処理を持たず、JWT は exp まで有効のままにする」は、RS に失効状態を確認する手段がない限り、FAPI 2.0 Security Profile Final §5.3.4(3) の要件を満たせません。

中間の解は、JWT を self-contained に保ったまま、サーバ側の状態をトークンごとではなく grant ごとに結びつけることです。失効した grant につき 1 行書けば、その grant 配下のすべての AT に効きます。発行時には何も書きません。

判断

既定は op.RevocationStrategyGrantTombstone です。失効状態は grant ごとに持ち、既定の発行経路は /token でデータベースに 1 行も書きません。

各 JWT AT は gid という private claim(OP 側の GrantID。RFC 7519 §4.3、omitempty)を持ちます。OP のエンドポイント(/userinfo、/introspect)は検証時に、gid をキーにした小さな grant 単位の tombstone テーブルを参照します。

  • カスケード(ログアウト、コード再利用、refresh chain の侵害)は、失効した grant 1 件につき tombstone を 1 行書きます。
  • /revoke による単一 AT の失効は、失効した jti 1 件につき deny-list を 1 行書きます。
  • 定常状態の行数は O(失効した grant 数 + 失効した jti 数) であり、O(発行数) ではありません。

ほかの戦略:

  • op.WithAccessTokenRevocationStrategy(op.RevocationStrategyJTIRegistry) は、AT 単位の監査記録が必要な組み込み側向けです。store.AccessTokenRegistry に発行ごとの shadow 行を残します。
  • op.RevocationStrategyNone は、RFC 6749 §4.1.2 の「SHOULD」の余地を明示的に受け入れる、非 FAPI デプロイ向けの第 3 の選択肢です。いずれかの FAPI プロファイル下では op.New が拒否します。

tombstone とレジストリのどちらの戦略でも、OP のエンドポイントはサーバ側の失効状態を参照できます。ただし、OP 側の戦略を選ぶだけでは FAPI Final への適合を示せません。RS も §5.3.4(3) を満たす必要があります。

opaque AT の経路(op.WithAccessTokenFormat(op.AccessTokenFormatOpaque))は、この戦略選択の影響を受けません。opaque の検証にはレコードの参照が必要なので、ストレージは本質的にトークン単位だからです。

失効の契機。 すべての契機は 1 つの所有者(internal/grants/teardown)を通り、契機ごとの影響範囲がそこで決まります。grant は 1 人の subject が 1 つの client 上に持つすべてのセッションで共有されるため、ほとんどの契機は意図的に grant 全体までは広げません。

契機失効させるリフレッシュトークン失効させるアクセストークンgrant への以後の発行
/end_session(subject が持つ各 grant について)grant のすべてgrant のすべて(JWT と opaque)止める
認可コードの replaygrant のすべてgrant のすべて止める
リフレッシュトークンの replay(猶予期間後の再利用)replay された chain。chain の根を解決できなければ grant のすべてgrant のすべて止める
リフレッシュトークンの /revokeその chain。chain の根を解決できなければ grant のすべてgrant のすべて止めない(他の chain と未使用の認可コードは引き換え可能)
リフレッシュのローテーション直前のトークン同じ chain から発行された opaque のもの止めない

「止める」は tombstone 戦略での挙動です。tombstone が、その grant のリフレッシュトークンと認可コードの引き換えも拒否します。

/revoke の行は、アクセストークンだけを対象にした tombstone(store.GrantTombstone.AccessTokensOnly)を書きます。store が任意のインターフェース store.IssuanceRevocationChecker を実装していれば、token endpoint は chain や認可コードがまだ発行できるかの判定でこの種別を無視できます。ローテーション時の chain 単位の opaque 失効には store.OpaqueAccessTokenChainRevoker が必要です。どちらも実装しない store や ChainID を保持しない store は、grant 全体の挙動に戻ります。これは失効が足りなくなる方向ではなく、多すぎる方向のずれです。/revoke は token.revoked を記録し、surface には refresh_chain を、grant のリフレッシュトークンへ切り替えた場合は refresh_grant を入れます。

別の client が提示した認可コードやリフレッシュトークン、redirect_uri や PKCE の検証に失敗したものは replay ではありません。何も消費せず、何も失効させません。上の表が動くのは、本物の replay だけです。

code 交換、refresh、token exchange、device code や CIBA の poll の最中に grant が失効しても、有効な資格情報は残りません。token endpoint は発行したものを保存したあとで grant が閉じていないかを再確認し、直前に発行した資格情報を失効させます。grant の teardown は、tombstone 方式に限らず、あらゆるアクセストークンの失効方式でこの閉鎖を記録します。

実装箇所 op/access_token_revocation.go、internal/tokens(gid claim)、op/store/grant_revocation.go、internal/grants/teardown、各バックエンドアダプタ

20. DCR の client_secret の保存 ​

登録、issuer、外向き fetch

仕様 RFC 7591 §3.2.1 は、登録応答の client_secret を任意(OPTIONAL)としています。RFC 7592 §2.1(読み取り)と §2.2(更新)も、応答ボディに client_secret を再掲することを許容していますが、要求はしていません。

衝突 「平文を保存しておき、GET のたびに再開示する」という読みは、実装が最も単純で、歴史的に一部の OP がそうしてきました。しかし、ストアに復元可能な平文を永続的に残すことになり、登録レコードが DB で最も機密度の高い行になります。

「POST の時点で hash 化し、以後は再開示しない」という読みは、OP 内の他の資格情報すべての保存方式と揃い、平文を復元できる場所をなくせます。代わりに、POST 応答(平文を 1 度だけ取得可能)と GET 応答(平文は二度と取得できない)の間に非対称が生じます。

判断

保存は hash のみです。平文は POST /register で 1 回だけ返し、GET /register/{client_id} では client_secret を再開示しません。

PUT /register/{client_id} が平文を再び返すのは、token_endpoint_auth_method が none から confidential 方式に変わったときだけです。confidential から confidential への更新は既存の hash を保つので、メタデータを編集しても secret は入れ替わりません。

平文に後からアクセスする必要がある組み込み側は、最初の POST 応答の時点で自前のコピーを保管します。OP は平文の一次保管者にはなりません。

ストレージは op/store/client.go の SecretHash だけで管理するので、すべての store.ClientStore アダプタに同じ方針が適用されます。

実装箇所 internal/registrationendpoint/manage.go、internal/registrationendpoint/register.go

21. RFC 7592 PUT で省略されたフィールド ​

登録、issuer、外向き fetch

仕様 RFC 7592 §2.2 は、次の 2 文で構成されています。

RFC 7592 §2.2 第 1 文(訳) 応答で返されるメタデータの値は、それ以前のクライアントに紐付いていた値を 置換 するものとし、追加してはならない。

RFC 7592 §2.2 第 2 文(訳) サーバはリクエスト中の null または空の値を、他の値と同様に無視してもよい。

衝突 第 1 文を厳密に読むと、PUT で grant_types を省略したら「grant_types を削除する」ことになります。クライアントは grant を何も使えなくなり、その後の /token 要求がすべて失敗します。第 2 文の MAY 句は、参照実装がそろってこの落とし穴を避けるために使う逃げ道ですが、エコシステムは単一の置換ポリシーに収束していません。

判断

省略された 既定値付き フィールドはサーバの既定値にリセットし、省略された 任意 フィールドは空にします。

  • 既定値付き: grant_types、response_types、token_endpoint_auth_method、application_type、subject_type、id_token_signed_response_alg。PUT で省略された場合は、レコードから消えるのではなく、OP がドキュメントで示す既定値に戻ります。
  • 任意のメタデータ(client_uri、logo_uri、policy_uri、tos_uri、contacts ほか)は、省略すると実際に空になります。
  • サーバ管理のフィールド — registration_access_token、registration_client_uri、client_secret_expires_at、client_id_issued_at — は、ボディに含まれていれば 400 invalid_request で拒否します。認証中のクライアントと一致しない client_secret の値も、同じステータスで拒否します。

「クライアントが設定したのか、サーバが既定値を埋めたのか」を区別するビットは永続化しません。既定値付きフィールドの通信路上の挙動は、どちらでも同じになるためです。

実装箇所 internal/registrationendpoint/manage.go(validateManageUpdateRequest)、internal/registrationendpoint/metadata.go(applyMetadataDefaults)

22. sector_identifier_uri の取得と loopback host ​

登録、issuer、外向き fetch

仕様 OIDC Core 1.0 §8.1 は、sector_identifier_uri の取得と、登録された redirect_uris の包含検証を必須としています。

OIDC Core 1.0 §8.1(訳) redirect_uris に登録された値は、その配列の要素に含まれていなければならない。さもなければ登録は失敗しなければならない。

timeout もボディサイズの上限も、仕様は定めていません。OIDC Registration §2 は、application_type=native の loopback host として localhost、127.0.0.1、[::1] を列挙しています。RFC 8252 §8.3 は、DNS rebinding を理由に、localhost を IP リテラルより推奨しない(NOT RECOMMENDED)としています。

衝突 「言語の既定値で取得する」は、実質的に「上流が応答するまで goroutine を保持し続ける」ことを意味し、応答ボディも無制限です。どちらも登録時の落とし穴です。

localhost については、OIDC Registration §2 と RFC 8252 §8.3 が一致しません。OIDC 側は native でこれを認め、OAuth 側は推奨しません。web クライアントが http://localhost/cb を登録する場合は、native クライアントが同じ登録をする場合とは別の問題です。

判断

取得は 5 秒のタイムアウト、64 KiB のボディ上限、HTTPS 限定、リダイレクト禁止、24 時間の成功キャッシュに制限します。

失敗はキャッシュしません。キャッシュヒット時も、その時点で登録しようとしているクライアントの redirect URI 集合に対して包含チェックを再実行します。そのため、同じ sector 配下の別クライアントが登録できる範囲がキャッシュによって広がることはありません。

後続の取得でリモート文書の正規化 hash が変わった場合、resolver は ErrSectorContentChanged を 1 回返し、古いキャッシュエントリを追い出します。その URI の次の正当な登録が、新しい文書でキャッシュを作り直します。RP 側の正当な更新は OP を再起動せずに回復でき、予期しない sector の変更は運用者に見える形で一度表に出ます。

取得の失敗や包含の不一致には 400 invalid_client_metadata を返します。原因は監査ログに記録しますが、応答ボディには載せません。上流の詳細(host、TLS の状態、部分的な byte 列)を漏らさないためです。

loopback host については、登録層が application_type で分岐します。

  • web クライアント(既定)は、127.0.0.1 と [::1] を http で受理します。文字列の localhost は、組み込み側が op.WithAllowLocalhostRedirectURIs() で明示的にオプトインしない限り拒否します。
  • native クライアント(application_type=native)は、OIDC Registration §2 に従い 3 種すべての loopback host を無条件で受理します。加えて、claimed https の redirect と、RFC 8252 §7.1 の reverse-DNS 形式の custom URI scheme(com.example.app:/cb など)も受け付けます。. を含まない custom scheme は、アプリ間で衝突するため拒否します。

authorize 時の loopback ポートワイルドカードは、別のクライアント単位のオプトイン(#4)で制御します。本ルールとは役割が分かれており、重なりません。

実装箇所 internal/registrationendpoint/sector_identifier.go、internal/registrationendpoint/metadata.go(validateRedirectURI / validateNativeRedirectURIScheme)

23. 外向き fetch の SSRF 境界 ​

登録、issuer、外向き fetch

仕様 OIDC Dynamic Registration、JAR、private_key_jwt、pairwise subject、Back-Channel Logout は、いずれも RP が管理するメタデータから OP に外向きの URL を取得させます。各仕様は「何を取得するか」は定めていますが、private network、loopback、DNS rebinding、custom TLS root に対する共通の SSRF 境界は定めていません。

衝突 conformance runner、内部 CA、公的に信頼されていない RP のネットワークでは、custom transport が必要になることがよくあります。一方、HTTP client 全体を差し替え可能にすると、呼び出し側が意図せず接続時の SSRF 防御まで外せてしまいます。custom transport を拒否すれば防御は単純ですが、正当なデプロイまで動かなくなります。

判断

外向きの信頼と外向きの到達範囲は、別の設定に分けます。custom transport だけでネットワーク境界が広がることはありません。

op.WithJWKSHTTPTransport で、組み込み側は RP JWKS 取得用の TLS の信頼設定と transport 設定を渡せます。SSRF の防御がどこまで効くかは、transport の種類で決まります。

  • *http.Transport: package 側が dial 関数を gate 付きの dialer に差し替えるため、接続時の SSRF deny-list はそのまま働きます。
  • それ以外の http.RoundTripper: 各リクエストとリダイレクトの URL だけを検査します。DNS rebinding を仕掛ける相手は防げません。
  • proxy 経由: 接続時の gate が見るのは proxy のアドレスです。宛先 URL は、引き渡す前に 1 回検査します。

private network の許可は、WithAllowPrivateNetwork(op.OutboundJWKS, op.OutboundJAR, ...) という明示的なポリシー設定が担います。

JAR の JWKS、client 認証の JWKS、sector_identifier_uri、Back-Channel Logout の宛先にも、同じ上限付き取得の方針を適用します。最初に URL の形を検証し、次に接続時のネットワーク検査を行い、最後に上限付きで応答を処理します。

実装箇所 op/options_session.go、internal/netsec、internal/securefetch、internal/jar、internal/endpointsupport/clientauth.go、internal/registrationendpoint/sector_identifier.go

24. scope を省略した Open DCR ​

登録、issuer、外向き fetch

仕様 RFC 7591 は、登録メタデータの scope を任意としています。IAT による登録では、Initial Access Token の発行者は運用者のコードであり、明示的な AllowedScopes ポリシーを付けられます。Open registration には、運用者がリクエストごとに発行するこうした枠がありません。

衝突 省略された scope を「public scope すべて」と扱うと、ボディがほぼ空の open registration の POST が、予想以上に強い client を作ります。「scope なし」と扱う方が安全ですが、client が scope を明示するか、組み込み側が既定値を設定する必要があります。

判断

Open registration では、省略された scope を空の登録 scope 集合として保存します。

その後の /auth で、client が登録していない scope を要求すると invalid_scope で拒否します。public な既定値を意図的に持たせたい組み込み側は、DynamicRegistration.OpenRegistrationDefaultScopes でオプトインします。この値は、構築時に OP の scope レジストリに対して検証されます。

IAT による経路は、運用者を信頼した広い既定値を維持します。IAT に AllowedScopes がなければ、public scope レジストリから既定値を取れます。

実装箇所 op/registration.go、op/options_validate.go、internal/registrationendpoint/handler.go、internal/registrationendpoint/register.go、internal/registrationendpoint/metadata_validate.go

25. assertion の replay 消費より前の DPoP nonce ​

プロファイルと discovery

仕様 RFC 9449 は、AS が token request に use_dpop_nonce を返し、client が新しい DPoP 証明で再送する形を認めています。RFC 7523 は、JWT client assertion に jti / 時刻の時間幅による replay 防止を要求しています。

衝突 token endpoint が先に client 認証を行うと、初回の request で client_assertion の jti を消費したあとに use_dpop_nonce を返すことになります。client が form body はそのまま、DPoP 証明だけを差し替えて再送すると、invalid_client / assertion replay で失敗します。nonce challenge は RFC 9449 が想定する回復経路であるにもかかわらずです。

判断

token と PAR の経路では、client 認証より前に DPoP nonce を検証します。

nonce が欠けている、または古い場合は、form に載った client_assertion を消費せずに nonce challenge を返します。DPoP 証明が受理できる状態になってから、通常の private_key_jwt の replay キャッシュが assertion を 1 回だけ消費します。

実装箇所 internal/tokenendpoint/authcode.go、refresh-token の token 経路、internal/parendpoint/par.go(internal/tokenendpoint/refresh_dpop_nonce_pkjwt_test.go で固定)

26. CIBA の polling とエラー ​

プロファイルと discovery

仕様 CIBA Core 1.0 は poll / ping / push の各 mode、slow_down、および §13 のエラー語彙を定義しています。poll の濫用に対する lockout の閾値は実装に委ねています。FAPI-CIBA は CIBA の上に署名付き request object の要件を重ねますが、conformance module は CIBA の形のエラーを期待します。

衝突 上限のない poll の段階制御では、client が slow_down を無視して auth_req_id の期限まで poll し続けられます。低い上限をハードコードすれば本番の保護にはなりますが、反復 poll を意図的に行う OFCS module を壊す場合があります。

また、/bc-authorize から JAR parser の詳細なエラーを返すとデバッグには便利です。しかし、invalid_request を期待する CIBA の endpoint から、JOSE 固有のエラー分類が通信路に漏れます。

判断

本ライブラリは CIBA の poll mode だけを実装し、slow_down 違反のカウンタに上限を置きます。

  • 既定の上限は 5 回です。CIBAGrant.MaxPollViolations で上げ下げでき、0 はライブラリの既定値を意味します。counter が uint8 なので、255 は実質的に lockout の無効化です。本番のデプロイでは有限の上限を保つべきです。
  • op.New は、負の CIBAGrant.DefaultRequestTTL と CIBAGrant.PollInterval も拒否します。FAPI-CIBA では、既定または最大の有効期限が 10 分を超える設定も拒否します。

/bc-authorize では、単一値のパラメータの重複を拒否し、request object の失敗は CIBA Core §13 に合わせて invalid_request に対応づけます。FAPI-CIBA では、JAR verifier が iat を要求し、request object の有効期間を 60 分に制限します。RP JWKS は、OP の他の箇所と同じ SSRF 対策済みの取得経路で取得します。

実装箇所 op/options_ciba.go、internal/ciba/polling.go、internal/cibaendpoint、internal/jar、op/op_builders.go

27. JWE 許可リストと nesting 上限 ​

JOSE と request object

仕様 RFC 7516 と RFC 7519 は、広い JWE アルゴリズムの集合と、入れ子になった JWT の形を許しています。原理的には、JWT が JWE に包まれ、その JWE がさらに別の JWE に包まれる、という形を取り得ます。

衝突 汎用の JOSE ライブラリが対応するすべての形を受理すると、OP が必要とする範囲を超えて暗号処理の対象が広がります。層ごとの平文の上限は各復号のメモリを守りますが、攻撃者が制御する入れ子全体の深さまでは制限できません。

判断

JWE はポリシーで閉じ、入れ子になった JOSE の走査に上限を置きます。

OP は次のように扱います。

  • 明示的な alg / enc の許可リストだけを受理する
  • 未知の crit を拒否する
  • 復号後の平文を 1 MiB に制限する
  • 11 層目の JOSE layer を ErrJWENestingTooDeep で拒否する

通常の暗号化された request object は最大でも 2 層(JWS を包んだ JWE)です。10 層の上限は将来のプロトコルの形に余地を残しつつ、再帰を無制限にしません。

実装箇所 internal/jose/jwe.go(JAR / PAR の検証から使用、internal/jar/verify_jwe_test.go と深い入れ子の暗号化 request object のシナリオで固定)

28. Custom grant のリフレッシュトークン ​

トークン、grant、失効

仕様 RFC 6749 §6 は、リフレッシュトークンを authorization server が発行する資格情報として扱っています。RFC 9700 §4.14.2 は、public client のリフレッシュトークンの replay を検出するため、sender-constrained refresh token またはローテーションのいずれかを AS に要求しています。ローテーションでは新しい token を発行し、旧 token を関係を保持したまま無効化し、再利用で侵害が分かったときは有効な token を失効させます。

衝突 custom grant は、意図的にハンドラが定義するものです。AccessToken と同じように、ハンドラが任意のリフレッシュトークン文字列を返せる設計は一見自然です。しかし、値と親子関係を OP が作っていない資格情報は、OP がローテーション、バインド、連鎖失効できません。逆にリフレッシュトークンを全面的に拒否すると、長寿命のサービス間の連鎖を OP 既存のローテーション機構に乗せたい custom grant や token-exchange のポリシーが作れません。

判断

ハンドラは意図だけを示し、リフレッシュトークンの値は OP が持ちます。

CustomGrantResponse.IssueRefreshToken は、OP に RefreshTokenStore 経由でリフレッシュトークンを生成・永続化させるフラグです。レコードは発行したアクセストークンと同じ grant 識別子と DPoP / mTLS の confirmation を共有するため、通常のローテーション、再利用時の連鎖失効、grant の失効、introspection の意味論がそのまま適用されます。ハンドラがリフレッシュトークン文字列を直接渡す経路はありません。token exchange も TokenExchangeDecision.IssueRefreshToken で同じ方針を通します。

発行は 5 つの条件で決まります。

  1. provider が refresh_token を提供している
  2. client がそれを使うよう登録されている
  3. 応答の subject(または束縛されたアクセストークンの subject)が空でない
  4. 応答の scope が空でない
  5. 応答の audience が 1 件以下である

いずれかを満たさなくても、アクセストークンの応答と、他の発行条件を満たす ID Token は成功します。リフレッシュトークンだけが省略され、custom_grant.refresh_dropped が理由を記録します。scope が空の場合も、落ちるのはリフレッシュトークンだけです。

一般的な custom grant の資格情報について、subject を保証するのは grant レコードではなくハンドラです。そのため、ハンドラが grant を記録しない限り、Provider.RevokeSubject はそれらを失効させず、期限切れか個別の失効まで有効なままです。pairwise subject の /userinfo は、それらに invalid_token を返します。アカウント削除時の失効は、ハンドラの責任です。Provider.ForgetSubject は RevokeSubject に加えて、その subject のブラウザセッション、ロックアウトの記録、認証 factor の記録も削除しますが、これらの資格情報には届きません。

token exchange は違います。その資格情報は subject_token の grant 配下に置かれるので、その grant を失効させればまとめて失効します。exchange で得たリフレッシュトークンも、subject の削除後はローテーションされなくなります。

実装箇所 internal/tokenendpoint/customgrant.go

29. device code 失効のカスケード ​

トークン、grant、失効

仕様 RFC 8628 は device authorization の状態遷移を定義していますが、ユーザが後からデバイスの登録を解除したとき、その device authorization から発行済みのアクセストークンをどう扱うかまでは定めていません。RFC 7009 は AS の revocation endpoint を定義していますが、デバイスの登録解除は標準の通信路上のリクエストではなく、組み込み側の UX 操作です。

衝突 失効が device code の行を拒否状態にするだけなら、以後のポーリングは止まりますが、発行済みのアクセストークンは exp まで使えます。一方、組み込み側が監査イベントを購読して RevokeByGrant を自前で呼ぶ設計にすると、各デプロイが out-of-band のつなぎのコードを忘れずに書くことに安全性が依存します。

判断

公開ヘルパが連鎖失効まで受け持ちます。 devicecodekit.Revoke は次の順で処理します。

  1. device code の行を拒否状態にします。
  2. device flow がその device code 用に割り当てた grant 識別子を再計算し(device code ID からの一方向の導出なので、device code そのものは再び公開されません)、その grant 配下に発行されたものを失効させます。JWT アクセストークンは Deps.RevocationStrategy に従い、opaque アクセストークンは Deps.OpaqueAccessTokens があれば、リフレッシュトークンの chain は Deps.RefreshTokens があれば対象になります。
  3. Deps.Grants を設定していれば、連鎖失効が終わったあとで最後に同意レコードを削除します。その同意が以後の認可リクエストを満たすことはなくなります。

どれかのクラスが失敗しても、残りは試行します。部分的な失敗は結合したエラーを返し、cascade_complete=false を記録します。同じ呼び出しを繰り返せば、残りを完了できます。

nil のまま残したサブストアは飛ばします。JWT のみの構成や、失効を out-of-band で行う構成でも、行は拒否状態になり監査イベントも出ます。ヘルパは、飛ばした連鎖失効を実行したかのようには扱いません。

device_code.revoked には、試行したクラスごとに 1 つずつ extra(revoked_access_tokens、grant_tombstone_written、revoked_opaque_access_tokens、refresh_token_cascade_complete、grant_record_deleted)と cascade_complete が入ります。運用者は、連鎖失効が想定どおりに完了しなかった場合を検知できます。

実装箇所 op/devicecodekit.Revoke

ブラウザ、セッション、UI 境界

仕様 OIDC Core と OAuth 2.0 は認可とトークンのプロトコルを定義していますが、OP がブラウザの cookie を使うこと自体は要求していません。cookie の暗号化は、本ライブラリが interaction / session を束縛するための実装の詳細です。

衝突 実装方針としては 2 つあり得ます。

  • 常に cookie key を要求する — ドキュメントは単純になり分岐も減りますが、client_credentials 専用の OP やブラウザを使わないデプロイでも、使わない secret を設定する必要が出ます。
  • ブラウザ認可が有効なときだけ要求する — サービス間通信専用の構成には正確で扱いやすい一方、どの grant が暗号化 cookie を必要とするかを、ハンドラを mount する前に option 層で判断する必要があります。

判断

WithCookieKeys は、有効な grant 集合に authorization_code が含まれる場合だけ必須です。既定の grant 集合には含まれています。

WithIssuer、WithStore、WithKeyset は無条件の構築要件です。authorization-code flow はブラウザの interaction の状態を暗号化した session / CSRF cookie で束縛するため、cookie key が必要です。authorization_code を明示的に外した client_credentials 専用のデプロイは、cookie key なしで起動できます。

この規則は validateCookieKeysRequired に集約しています。将来、authorize endpoint の cookie を必要とする grant が増えても、ハンドラのコードを変えずにこの検証へ追加できます。個々の鍵は、AES-256-GCM に合わせて 32 byte ちょうどでなければなりません。

実装箇所 op/options_validate.go(ドキュメントは 必須オプション)

31. Grant Management はオプトインのまま ​

トークン、grant、失効

仕様 OAuth 2.0 Grant Management はまだ IETF draft です。安定した grant_id と、create、replace、merge、query、revoke の 5 つの action を定義していますが、RFC として公開されるまでに通信路上の形が変わる可能性があります。

衝突 長期間有効な同意を扱う OP では、grant に名前を付け、クライアントが後から照会したり失効させたりできることに明確な価値があります。一方、draft の機能を既定で有効にすると、変わり得る通信契約をすべてのデプロイに晒し、運用者がまだ支える準備のない意味論を discovery で広告してしまいます。

判断

Grant Management は既定で無効で、明示的に experimental として扱います。

op.WithGrantManagement(op.GrantManagement{Actions, ActionRequired}) には、OP が受け付ける action の集合を正確に列挙します。同じ集合を discovery で広告し、PAR / 認可時(create / replace / merge)と endpoint 呼び出し時(query / revoke)の両方で強制します。未知の action、重複した action、空の action 集合、action と grant_id の不整合は、構築時またはリクエストの検証時に拒否します。

有効にすると、token 応答に grant_id が載り、/grant_management の query / revoke endpoint が mount されます。

  • 存在しないこと、または別 client の所有であることが確定した grant には 404 を返します。
  • 存在を判断できない store の障害では、カスケードを試みずに 500 server_error を返します。
  • カスケードの失敗も 500 になり、失敗の監査イベントを出し、grant は再試行・照会できる状態に残ります。
  • 別のリクエストが先に削除を終えていた最後の delete だけは、冪等な 204 です。

draft の部分はこの option の背後に隔離しています。将来、通信互換性を壊す draft の更新が来ても、偶発的な既定の変更ではなく、明示的な移行として扱えます。

実装箇所 op/grant_management.go、internal/parendpoint、internal/authorizeendpoint、internal/grantmgmtendpoint、internal/tokenendpoint

32. 認可ごとの状態は資格情報に持たせる ​

トークン、grant、失効

仕様 OIDC Core 1.0 は auth_time、acr、amr を ID Token の背後にある認証イベントの性質と定め、claims リクエスト(§5.5)をそれを運んだ認可リクエストに紐づけています。RFC 9396 は、authorization_details をそのリクエストでリソースオーナーが許可した内容に結びつけています。

衝突 ライブラリは (subject, client) の組ごとに store.Grant を 1 つ持ち、同意を引き継ぐために認可をまたいで再利用します。資格情報を使う時点でこの共有レコードから auth_time、claims、details を読むと、後の認可が先の認可の報告内容を書き換えてしまいます。同じユーザが 2 回目にログインしたあとで引き換えた認可コードは、ID Token に 2 回目のログインを示し、2 回目のリクエストが求めた claims を開示し、許可していない authorization_details まで拾います。

判断

資格情報は、自分を発行した認可の状態を持ちます。

store.AuthorizationCode、store.RefreshToken、store.OpaqueAccessToken が、AuthTime、ACR、AMR、AuthorizationDetails、claims リクエストを保持します。リフレッシュのローテーションはそれらを後継に引き継ぐので、chain の ID Token、アクセストークン、introspection 応答は、その chain 自身の認可を表します。

  • Grant Management の replace による details の取り下げは、有効なトークンにも反映されます。後の認可が足した内容は届きません。
  • claims パラメータなしでユーザ操作を挟まずに返した認可は、自分のリクエストが求めた claims だけを開示します。同じ grant 上の以前のリクエストが求めた claims は開示しません。

認可コードやその refresh chain から発行する JWT アクセストークンは、userinfo_claims という private claim も持ちます。値は claims リクエストの userinfo メンバー、リクエストがなければ {} です。/userinfo はここから claims を開示します。リソースサーバは無視してかまいません。この名前は予約されているので、custom grant が extra claims で設定することはできません。

AuthorizationRecorded が、状態を持つ行の目印です。目印のない行(たとえば、これらのフィールドを永続化しない store が書いた行)は grant の値に戻ります。そのため、独自の store はフィールドを永続化するまで、共有 grant の挙動のままで動作します。

実装箇所 internal/tokenendpoint、internal/tokens、op/store

33. 再利用セッションでのステップアップ ​

ブラウザ、セッション、UI 境界

仕様 OIDC Core 1.0 §3.1.2.1 は prompt と max_age を、RFC 9470 は acr_values によるステップアップを定義しています。ユーザがすでにセッションを持つシングルサインオンの場面で、OP 自身のポリシールールがどう働くかは、どちらも規定していません。

衝突 RuleClient や RuleScope などの LoginFlow ルールは、特定の client や scope を追加の factor の背後に置きます。ルールを factor chain の実行中にしか評価しないと、primary factor だけで確立したセッションが、要求を覆うキャッシュ済みの grant や first-party の同意スキップを通って、ルールに一度も当たらないまま保護された client や scope に届いてしまいます。

判断

セッションから発行する前に、ルールと Decider を評価します。chain がちょうど Primary を完了したものとして扱います。

どれかがステップを要求すれば、/auth は発行せず、セッションの subject から chain を実行します。prompt=none なら interaction_required を返します。

ルールの step は、その step が記録する値をセッションの amr がすでに持っていれば、完了として数えます。

  • パスワードは pwd
  • TOTP、recovery code、メール OTP は otp
  • passkey は hwk(user verification を要求しない step なら hwk または swk)

amr は 3 種類の otp factor を区別できないため、互いに代わりになります。op.StepOneOf は中のどれかの step が満たされれば完了とみなします。op.ExternalStep と op.StepCaptcha はセッションでは満たされず、毎回あらためて求めます。新しい factor が必要な relying party は、max_age や prompt=login を送るか、セッションの水準を超える必須の acr を要求します。リスクに基づくルールは chain の中でだけ発火します。

セッションは、設定済みの op.ACRPolicy が要求値のいずれかをセッションの水準で受理する場合にも、acr_values と必須の acr claim を満たします。op.ACRLevels{"urn:example:mfa": op.AAL2} なら、AAL2 のセッションは再利用され、AAL1 のセッションは引き続き再認証になります。応答が報告する acr を選ぶポリシーが、セッションがそれを提供してよいかも判断します。

ステップアップ後に発行するトークンは、ステップアップした factor だけでなく、セッションの acr、amr、auth_time を保ちます。リスク評価器は AMRSoFar でセッションの認証方式を参照でき、ステップアップの factor は login.* ではなく mfa.* イベントとして監査されます。

評価は拒否側に倒します。

  • Rule.When が panic すれば、一致とみなします。
  • Decider が panic する、または nil・nil ポインタ・未知の形を返す場合は拒否します。
  • factor の interaction.Result が、先行する factor やアカウントチューザが束縛した subject と別の subject を示した場合は、chain をサーバエラーで終わらせます。この検査は、プロンプトなしで Begin の中で完了する factor にも及びます。

実装箇所 internal/authorizeendpoint、op/rule.go、op/decider.go

34. ID-JAG の署名アルゴリズム ​

トークン、grant、失効

仕様 Identity Assertion JWT Authorization Grant draft(draft-06)は、IdP とリソース認可サーバの双方に RS256 の実装を必須とし、リソース側の ES256 は SHOULD にとどめています。

衝突 この OP は、発行するものすべてを 1 つのアルゴリズムで署名します(#11)。1 種類のトークンのために別のアルゴリズムを足すと、まだ変わり得る draft のために単一アルゴリズムの方針を開き直すことになります。既定の ES256 のままでは、必須アルゴリズムしか実装しないリソース認可サーバは、この OP の ID-JAG を検証できません。

判断

ID-JAG は OP の発行アルゴリズムで署名し、ID-JAG 専用の設定はありません。

  • 既定の ES256 では、IdP 側の役割を RFC 準拠マトリクスで partial と明記しています。
  • op.WithSigningAlgorithm(op.AlgRS256) で構築した OP は、ID-JAG を draft の必須アルゴリズムである RS256 で署名します。同じ切り替えで、他のすべての発行トークンも RS256 になります。

任意の discovery メンバー id_jag_signing_alg_values_supported は、どちらのアルゴリズムでも公開しません。受け取る assertion の検証は RS256、PS256、ES256、EdDSA を受け付けるので、リソース認可サーバ側の役割には影響しません。

ID-JAG の相手が RS256 を要求する一方で、他の RP が ES256 を要求する組み込み側には、外部署名または複数アルゴリズムでの署名が必要です。これは別の判断になります。

実装箇所 internal/customgrant/tokenexchange、internal/tokens

35. 単回使用の assertion ​

トークン、grant、失効

仕様 RFC 7523 §3 は jti を任意とし、単回使用はサーバの選択としています。ID-JAG draft は、クライアントが同じ grant を有効期間内に再提出して、追加のアクセストークンを得ることを許しています。

衝突 再提出を許すと、クライアントは IdP に戻らずにアクセストークンを更新できます。一方で、漏れた assertion が有効期間いっぱい使える bearer の資格情報になります。assertion の有効期間を短くしても、その時間幅は狭まるだけで、なくなりはしません。

判断

jti は必須で、plain も ID-JAG も、assertion は一度だけ引き換えられます。

jti は専用の名前空間で消費済み jti ストアに記録します。replay の検査は最後に行うため、拒否された assertion が jti を消費することはありません。受け付ける有効期間は 5 分に 60 秒の leeway を足した値が上限で、どちらも緩める設定はありません。

最初のアクセストークンが失効したあとに新しいトークンが必要なクライアントは、新しい assertion を取得します。複数ノードの構成ではストアを共有する必要があります。共有しないと、replay できる時間幅が assertion の有効期間と同じ長さになります。

実装箇所 internal/assertiongrant、internal/tokenendpoint

36. client_id URL の取得上限 ​

登録、issuer、外向き fetch

仕様 Client ID Metadata Document draft は、任意の HTTPS URL を client_id として使えるようにしています。認可サーバは、認証されていない相手のリクエストを根拠にその URL を取得することになります。draft は SSRF のリスクに触れて上限を示唆していますが、値は実装に委ねています。

衝突 取得は攻撃者が選んだ URL で始まるため、上限の値はどれも攻撃対象の広さを決める設定です。上限を設定可能にすると、扱いにくい 1 つのクライアントのために緩めてしまう余地が生まれます。固定にすると、文書が極端に大きい、または遅いクライアントは拒否されます。

判断

取得は他の外向き fetch と同じ接続時の SSRF gate を通り、上限は固定です。これらの値に option はありません。

  • URL: 取得するのは、path を持ち、query・fragment・userinfo・dot segment を含まない https URL だけです(255 バイト以内、ホストは ASCII)。それ以外は未知のクライアントとして扱い、取得しません。
  • 応答: 200 かつ application/json または application/*+json でなければなりません。リダイレクトは追わず、本文は 5 KiB まで、タイムアウトは 5 秒です。
  • ネットワーク: private、loopback、link-local、メタデータのアドレスは、接続時に拒否します。
  • キャッシュ: 文書は Cache-Control の期間を 60 秒から 24 時間の範囲に丸めてキャッシュします。取得失敗や不正な文書は 5 秒だけ記憶し、同じ URL をリクエストのたびに取得し直さないようにします。

op.WithAllowPrivateNetwork(op.OutboundClientIDMetadata) は、プライベートネットワーク上の RP 向けに、private な宛先と http を許可します。文書内の jwks_uri には効かず、そちらは op.OutboundJWKS で管理します。

取得失敗の理由は audit にだけ出し、クライアントには未知のクライアントとして見せます。同じ ID の登録済みクライアントがあれば常にそちらを優先し、取得は行いません。

実装箇所 internal/cimd、internal/securefetch

37. URL で識別されるクライアントの同意画面 ​

登録、issuer、外向き fetch

仕様 MCP の認可仕様は、認可画面に redirect URI のホストを示すことを求め、loopback の場合は警告することを推奨しています。draft は、client_id URL のホストを表示することを推奨しています。

衝突 URL で識別されるクライアントには、運用者が redirect URI を審査する登録の段階がありません。誰でも著名な client_id URL を流用し、その文書が正当に列挙している redirect URI、たとえば攻撃者の端末の loopback アドレスへユーザを送れます。client_id のホストだけでは両者を見分けられません。信頼を作る定番の方法はロゴの表示ですが、同意画面から取得するとクライアントのホストに閲覧者を追跡されます。

判断

同意画面には client_id URL のホスト、要求された redirect_uri のホスト、そして redirect が loopback のときの警告を表示します。ロゴは表示しません。

  • URL で識別されるクライアントには first-party の同意スキップを適用しません。そのような URL を op.WithFirstPartyClients に入れると、構築時に拒否します。
  • これらのクライアントには、private_key_jwt でも PKCE を必須にします。
  • 独自の consent driver は interaction.ClientView(MetadataDocumentHost、RedirectHost、RedirectLoopback)で同じ値を受け取るので、表示することが期待されます。

実装箇所 internal/authorizeendpoint、op/interaction、internal/authorize

38. メタデータ文書の変更 ​

登録、issuer、外向き fetch

仕様 draft は、redirect URI や鍵などクライアントのメタデータが変わったとき、認可サーバが既存の grant を無効にする必要がありうると述べています。

衝突 変更時に失効させるには、OP が取得した文書を前回のものと比較し、古い写しを保持し、どの変更を対象とするかを決める必要があります。それを行わない場合、jwks を差し替えた文書の新しい鍵で、変更前に発行されたリフレッシュトークンを引き換えられます。

判断

新しい文書は次回の取得から効き、既存の grant とリフレッシュトークンは失効させません。

これを悪用するには、攻撃者が文書を配信するホストを乗っ取り、さらにそのクライアントのリフレッシュトークンを盗む必要があります。両方が必要で、しかも public クライアントではリフレッシュトークン単体がすでに bearer の秘密です。

redirect URI と鍵は使うたびに最新の文書から読み直すので、redirect_uris から外したエントリはキャッシュが切れた時点で使えなくなります。文書が消えた場合は、次のリフレッシュで未知のクライアントとなり、invalid_client で失敗します。

draft が変更時の失効を必須にした場合は、この判断を見直します。

39. メタデータ文書の取得量 ​

登録、issuer、外向き fetch

仕様 draft は、認可サーバが多数の URL を取得させられる危険を指摘し、レート制限を示唆していますが、どこで行うかは定めていません。

衝突 origin ごとの予算を持つと、汎用の HTTP レート制限を意図的に組み込み側へ委ねているライブラリの内部に、もう 1 つレート制限が生まれます。何の上限もなければ、OP はリクエストの増幅器になります。

判断

取得量は共有の外向き gate で抑え、origin ごとの予算は持ちません。絞り込む場所は ClientIDMetadataDocuments.Allow です。

  • 取得は、JAR やクライアント JWKS の取得と、プロセス全体の外向き負荷 gate(同時 64)を共有します。
  • 同じ URL への同時要求は、1 回の取得にまとめます。
  • 失敗は 5 秒キャッシュします。

したがって最悪でも、同時 64 件の取得が、それぞれ 5 秒と 5 KiB で打ち切られます。gate が埋まっているときは server_error で失敗し、その失敗はキャッシュしません。

ClientIDMetadataDocuments.Allow は、取得の前に、キャッシュヒットの場合も含めて毎回 client_id URL を受け取ります。組み込み側はここで許可リストや独自のリミッタを使って絞り込み、絞った内容は即時に効きます。

実装箇所 internal/cimd、internal/remotecache

40. DPoP 証明のエラーコード ​

プロファイルと discovery

仕様 RFC 9449 §7 は、サーバが拒否した証明に対するエラーコードとして invalid_dpop_proof を、§8 はサーバが求める nonce を欠いた証明に対する use_dpop_nonce を定めています。

衝突 /token、/par などフォームポストを受けるエンドポイントは、不正なリクエストをすべて RFC 6749 §5.2 のコードで返しています。ヘッダ 1 つのために別のコードを設けると、RP のライブラリは DPoP のためだけに分岐を足すことになります。不正な証明への対処は、ほかの不正なリクエストと変わりません。

判断

フォームポストのエンドポイントは、形式不正・署名不正・時刻外れ・再送・不一致の証明に 400 invalid_request を返します。

description には RFC 9449 の言い回しで失敗の種類を書き、内部の原因は返しません。

クライアントの対処が異なる 2 つのコードだけは、RFC のとおりに残します。

  • nonce が欠けている、または古い場合は use_dpop_nonce を返し、新しい値を DPoP-Nonce ヘッダに載せます。クライアントはその値で再送します。
  • 保護リソースが証明や誤った方式で提示されたトークンを拒否する場合は、401 と WWW-Authenticate: DPoP error="invalid_token" を返します(DPoP を参照)。

実装箇所 internal/dpop