メインコンテンツまでスキップ

v0.13.0 影響確認

本ドキュメントは v0.13.0 に含まれる破壊的変更・挙動変更と、その利用者側(RP / テナント運用者)への影響をまとめる。

リリースに向けて対象変更が確定し次第、本ドキュメントへ追記していく。各変更は独立したセクション(## N. <変更>)として追加し、あわせて下記「影響まとめ」表に 1 行加える。

トリアージ完了(2026-09-08 時点 / v0.13.0 リリース時点)

v0.12.0(2026-07-06 リリース)以降のマージ 58 件をトリアージし、🔴 破壊的 / 🟡 挙動変更 / 🔒 要点検 に該当するものを本ドキュメントへ登録した。🟢 追加・⚙️ 内部のみの変更は「付録: 登録不要と判定した変更」に一覧で残してある(「未対応」と「そもそも無い」を区別するため)。

洗い出しコマンドは git log v0.12.0..main --merges。トリアージの経緯は Issue #1797。

アップグレード前に必ず読むもの: 3.1 iat / nbf の 60 秒上限・4. PAR 強制・6.2 federation 設定更新の全置換化・8.3 DBマイグレーション・12.1 JWT アクセストークンの aud・13.1 client_credentials の confidential 限定・13.2 Basic クレデンシャルのデコード・13.3 Basic の Base64 アルファベット・14. 番号マッチングの device_id。

影響まとめ​

#変更種別対象
1http_requests(複数リクエスト)実行時に、response_resolve_configs で解決したステータスが 400 / 500 に丸められなくなった(429 / 503 等がそのまま返る)🟡 挙動変更execution.function: http_requests かつ response_resolve_configs を設定している構成
1フェデレーションの callback も同様に丸めなくなった(userinfo / token リクエスト / JWKS 取得の失敗)🟡 挙動変更全フェデレーション構成(standard / facebook / oauth-extension)
1standard / facebook の $.http_request.response_headers.* が配列から文字列に変わった(oauth-extension と統一)🟡 挙動変更userinfo_mapping_rules でレスポンスヘッダーを参照している構成
1standard / facebook のステータスコードのキーが staus_code → status_code(旧キーは削除)🔴 破壊的userinfo_mapping_rules で staus_code を参照している構成
2external-api-authentication の認証結果に interaction ごとの内訳(interactions)が追加された🟢 追加認証結果を参照する構成(authentication-policy / /authentication-status のレスポンス)
2ciba-delegation テンプレートの2要素目が端末承認なしで成立する設定だったため修正(ciba_start を3回呼ぶだけで通った)🔒 要点検config/templates/use-cases/ciba-delegation/ をベースに構築しているテナント。更新しても既存の設定は変わらないため、手動での修正が必要
3client_assertion / Request Object / DPoP proof の iat / nbf が 60 秒より先の未来なら拒否されるようになった(全プロファイル)🔴 破壊的private_key_jwt / client_secret_jwt / JAR を使う全クライアント
3Request Object への request / request_uri 混入検知がキーベースになった🟡 挙動変更空文字・非文字列で当該キーを積んでいるクライアント
3JWT の登録済みクレームが null / 非文字列でも 500 にならず 400 / 401 を返すようになった🟡 挙動変更全テナント(改善方向・未認証到達可の穴を塞ぐ)
4require_pushed_authorization_requests: true が全プロファイルで実際に強制されるようになった(従来は discovery に広告されるだけの dead flag)🔴 破壊的当該フラグを true にしているテナント
5外部ソース(federation / 外部API / 外部パスワード / 外部トークン)で解決した1要素目のユーザーが既存ユーザーに重ねる形になり、そのセッションのトークンに載るクレームが増える🟡 挙動変更外部ソースで1要素目を解決する全構成
5user_resolve の custom_properties がキー単位マージになった(従来は全置換)🟡 挙動変更複数の認証方式が custom_properties を書く構成
5認証 executor で $.request_attributes.* が解決するようになった(従来は常に null)🟡 挙動変更external-api-authentication のマッピング
5user_resolve のマッピングから $.user を参照できるようになり、1要素目は user_mapping_rules が 2 回評価される🟡 挙動変更user_mapping_rules に now / uuid4 / random_string を書いている構成
6authorization-server の GET が jwks(署名鍵の秘密部)を返すようになった。監査ログの before/after にも入る🟡 挙動変更 / 🔒 要点検管理APIの利用者・監査ログの保管
6federation 設定の更新が全置換セマンティクスになった🔴 破壊的federation 設定を部分更新している自動化
6テナント更新で name が反映されるようになった(従来は黙って無視)🟡 挙動変更テナント更新APIの利用者
6federation 設定の一覧が disabled も返すようになった🟡 挙動変更一覧を件数・全件有効前提で扱っている自動化
6管理APIのレスポンスにフィールドが増えた(authentication-policy の policies / grant の grant_type 他 / federation の sso_provider)🟡 挙動変更レスポンスを厳密なスキーマで検証しているクライアント
6fapi20_scopes が GET で返るようになった。従来は往復のたびに黙って失われ、FAPI 2.0 固有の検証が掛からない状態になっていた🔒 要点検GET → 編集 → PUT を自分で書いていて、かつ FAPI 2.0 を使う構成のみ。設定ファイルを正として PUT する運用は該当しない。すでに往復した環境は更新しても復元されない
7ui_locales / acr_values が要求順を保持するようになった(DB に保存される値の順序も変わる)🟡 挙動変更保存値の順序に依存している構成
8FAPI 2.0 Security Profile Final プロファイルと DPoP (RFC 9449) を追加。DBマイグレーション 2 本が必要🟢 追加 / ⚙️全環境(マイグレーション必須)
8/userinfo が 401 時に WWW-Authenticate チャレンジを返すようになった🟡 挙動変更401 応答をヘッダー無し前提で扱っているクライアント
9認可画面の既定が同梱静的ページから app-view に、admin テナントの cookie_same_site が None → Lax に(テンプレート)🔒 要点検テンプレートからセットアップ済みの既存テナント
10同意画面で配列クレームの要素を選べるようになった(view-data に claim_values、/authorize に granted_claim_values)🟢 追加claims:* スコープで配列の custom_properties を解放している構成
10view-data のレスポンスに claim_values が増えた🟡 挙動変更view-data を厳密なスキーマで検証しているクライアント
11redirect_uri を RFC 9700 §2.1 の完全一致で照合する redirect_uri_exact_match_required を追加(既定 OFF・挙動は現状のまま。次のメジャーで既定を反転予定)🟢 追加 / 🔒 要点検OAuth 2.0 プロファイル(openid を含まない認可リクエスト)を使う構成
12JWT アクセストークンに aud が入るようになった(未設定テナントは issuer)🔴 破壊的JWT を自前検証し、aud があるときだけ照合しているリソースサーバー
12client_credentials のトークンと introspection に sub(= client_id)が入るようになった🟡 挙動変更sub の存在をユーザーの存在と同一視している実装
12複数のリソースにまたがるスコープ要求が invalid_scope で拒否されるようになった🟢 追加scope_resource_mapping を設定したテナントのみ
12AccessToken#hasSubject() を削除🟡 挙動変更idp-server をライブラリとして組み込んでいる場合
13token_endpoint_auth_method: none のクライアントが client_credentials を使えなくなった(従来は無認証でトークンが取れた)🔴 破壊的none かつ grant_types に client_credentials を持つクライアント
13Basic 認証のクレデンシャルを RFC 6749 Appendix B に従ってデコードするようになった(+ はスペースとして解釈)🔴 破壊的平文の client_secret に % + を含む client_secret_basic のクライアント
13Basic の Base64 が標準アルファベットのみになった(URL-safe で組んでいる非準拠クライアントは落ちる)🔴 破壊的client_secret_basic かつ符号化結果に + / 相当が出るクライアント
14番号マッチングの検証に device_id が必須になり、認証トランザクションに紐づく認証デバイスと照合されるようになった🔴 破壊的authentication-device-number-matching を使う構成(認証デバイスアプリの改修が必要)
14認証デバイスが紐づいていない認証トランザクションでは番号マッチングが成立しなくなった(fail-closed)🔴 破壊的認証デバイス未登録の利用者に番号マッチングを課している構成
15external-api の execution に渡る $.user 投影が利用者の確立源で判定されるようになった。認可エンドポイントの login_hint だけで埋まった利用者は投影されない🟡 挙動変更external-api の execution で $.user.* をマッピングし、login_hint 経路に依存している構成
15password / sms / email の execution からも $.user.* を参照できるようになった🟢 追加該当 interaction で http_request を使う構成
16クライアントの custom_properties が client_attributes に載るようになり、認証デバイスからも読めるようになった🔒 要点検クライアントの custom_properties を設定しているテナント。内容が認証デバイスに渡って良いか確認が必要

種別: 🔴 破壊的 / 🟡 挙動変更 / 🟢 追加 / ⚙️ 内部・運用 / 🔒 要点検(更新では直らず、設定の見直しが必要)


1. http_requests で response_resolve_configs のステータスが丸められなくなった(Issue #1783 / PR #1796)​

response_resolve_configs で 429 / 503 などにマップしても、execution.function: http_requests(複数リクエスト)の interaction では 400 か 500 に丸められていた。単発の http_request は元のコードを保持していたため、同じ設定でも executor によって結果が変わっていた。

1.1 Before / After​

上流の応答設定した mapped_status_codeBeforeAfter
500(レート制限を示す業務コード)429400429
503(サービス時間外)(マップなし・透過を期待)500503

とくに 429 が 400 に落ちるため、「利用者の入力誤り」と「レート制限・混雑」がクライアントから区別できなかった。429 にマップしないほうがまだ安全(500 のままなら障害系として扱える)という、設定意図と逆の判断を強いられていた。

1.2 挙動変更(注意)​

mapped_status_code を 4xx / 5xx に設定している構成: クライアントが受け取る status が変わる。たとえば 429 を設定していた場合、これまで 400 だったものが 429 になる。ステータスコードで分岐しているクライアントは追従が必要。

4xx / 5xx の別は変わらないため、isClientError() / isServerError() 相当の粒度でハンドリングしているクライアントには影響しない。

透過するコードは以下のとおり。

mapped_status_codeinteraction が返す status
401 / 403 / 404 / 408 / 409 / 429 / 500 / 502 / 503 / 504そのまま
上記以外の 4xx(410 / 422 / 451 等)400
上記以外の 5xx(501 / 507 等)500
400 未満(200 / 202 / 204 等)200(成功扱い)

丸め規則(列挙外の 4xx → 400 / 5xx → 500、400 未満 → 200)は変更前後で共通。本変更は http_requests でも列挙されたコードが透過するようになったもの(http_request は従来から透過)。

1.3 フェデレーション側(Issue #1800 / PR #1802)​

同じ丸めがフェデレーションの callback にもあり、しかも2段あった。

上流の応答(429)
→ UserinfoExecutionStatus 3値 … ここで 400 に丸め
→ FederationInteractionStatus 3値 … ここでも 400 に丸め
→ callback のレスポンス

両方に 401 / 403 / 404 / 408 / 409 / 429 / 500 / 502 / 503 / 504 を追加した。

対象BeforeAfter
上流 userinfo が 429 を返した400429
上流 userinfo が 503 を返した500503
response_resolve_configs で 503 にマップ500503
token リクエストの失敗400 / 500上流の実ステータス
JWKS 取得の失敗400 / 500上流の実ステータス
ID Token 検証の失敗400400(変更なし)

FederationInteractionStatus は userinfo 専用ではないため、token リクエストと JWKS 取得の失敗も上流の実ステータスで返るようになる。

ID Token 検証の失敗は対象外です。上流のHTTPステータスという概念がなく、OidcFederationInteractor が 400 を直接渡しているため、変更前後で同じです。

standard(Google / Azure AD 等)・facebook・oauth-extension の全プロバイダー型が対象。とくに standard / facebook は response_resolve_configs を使えないため、上流の実ステータスだけが情報源であり影響が大きい。

レスポンスヘッダーの形を統一​

standard / facebook は $.http_request.response_headers.* に HTTPヘッダーを配列のまま入れていた(["application/json"])。oauth-extension は先頭の値だけを取り出しており、同じ書き方でもプロバイダー型によって形が違った。

oauth-extension 側に揃えて文字列にした。

BeforeAfter
standard / facebook["application/json"]"application/json"
oauth-extension"application/json"変更なし

userinfo_mapping_rules でヘッダーをクレームに写している場合、値が配列から文字列に変わる。配列前提の変換関数を挟んでいれば見直しが必要。同名ヘッダーが複数ある場合は先頭の値のみになる。

ステータスコードのキー名を修正(🔴 破壊的)​

standard / facebook のステータスコードのキーは staus_code(タイポ)でした。status_code に修正し、旧キーは削除しています。

BeforeAfter
standard / facebook$.http_request.staus_code$.http_request.status_code
参照している設定は無言で壊れます

staus_code を参照している userinfo_mapping_rules があれば、status_code に書き換えてください。参照パスが解決しなくなってもエラーにはならず、値が null になるだけです(#1646)。設定の登録も GET も成功するため、実行時まで気づけません。

移行の確認は1回で済みます​

ヘッダーの形とキー名の変更は、どちらも standard / facebook の $.http_request.* を参照する userinfo_mapping_rules が対象です。同じ箇所を見ればよいため、確認は一度で済みます。

なお、どちらもリポジトリ内に参照する設定・テスト・ドキュメントが存在しないことを確認しています(ステータスコードを参照する例はドキュメントに一度も現れていませんでした)。

4xx / 5xx の別は変わりません

isClientError() / isServerError() 相当の粒度でハンドリングしているクライアントには影響しません。

1.4 移行手順​

設定変更・データ移行は不要。response_resolve_configs を http_requests で使っている場合のみ、クライアント側のステータスコード分岐を確認する。

これまで丸められた 400 / 500 を前提にしていた場合は、意図した mapped_status_code が返るようになるため、設定を書いたときの意図どおりに動くようになる方向の変更。

1.5 動作確認​

ケーステスト
http_requests でも丸められず 429 が返るe2e/.../integration/authentication-interactors/integration-02-authentication-interactor-response-resolve-configs.test.js
単発 http_request の透過(従来どおり)同上
フェデレーション callback が上流の 429 を返すe2e/.../usecase/advance/advance-18-federation-userinfo-status-code.test.js
フェデレーション callback が response_resolve_configs の 503 を返す同上
standard / facebook プロバイダーが上流ステータスを保持するOidcExecutorUserinfoStatusTest(federation-oidc unit)
両 enum の分類・fromStatusCodeUserinfoExecutionStatusTest / FederationInteractionStatusTest

1.6 関連​

  • HTTP Request Executor — Response Resolver 節
  • HttpRequestsAuthenticationExecutor#createErrorResult / AuthenticationExecutionStatus(libs/idp-server-core/.../authentication/interaction/execution/)
  • UserinfoExecutionStatus / FederationInteractionStatus / StandardOidcExecutor / FacebookOidcExecutor(フェデレーション側)
  • v0.12.0 の 2 番(response_resolve_configs の配列統一・authentication 側での実行時適用)の続き。v0.12.0 影響確認

2. external-api-authentication の認証結果に interaction ごとの内訳を追加(Issue #1771 / PR #1804)​

external-api-authentication は1つの設定に複数の interaction を持てますが、認証結果はすべて external-api-authentication という単一のキーに集約されていました。そのため authentication-policy から「どの interaction が成功したか」を区別できませんでした。

2.1 何が書けなかったか​

// 3つの interaction を通したいが、合計しか参照できない
{ "path": "$.external-api-authentication.success_count", "operation": "gte", "value": 3 }

これは1つの interaction を3回呼んでも成立するため、必須の interaction を通さないまま認証を完了できました。failure_count / lock_conditions も同様で、interaction ごとに試行回数の上限を変えられませんでした。

2.2 変更内容​

結果に interactions を追加しました。合計は従来どおり全 interaction の和です。

"external-api-authentication": {
"success_count": 3,
"interactions": {
"step-a": { "success_count": 1, "failure_count": 0, "call_count": 1 },
"step-b": { "success_count": 2, "failure_count": 0, "call_count": 2 }
}
}

ポリシーからは $.external-api-authentication.interactions.step-a.success_count で参照します。

2.3 出荷テンプレートに認証バイパスがありました(🔒 要点検)​

内訳を追加する過程で、ciba-delegation の出荷テンプレートがこの問題を実際に踏んでいることが判明しました。

ciba-delegation は3つの interaction(ciba_start → ciba_poll → userinfo)で構成されますが、2要素目の条件は合計で書かれていました。

{ "path": "$.external-api-authentication.success_count", "operation": "gte", "value": 3 }

ciba_start は CIBA リクエストを開始するだけで端末の承認を待たず、呼ぶたびに成功します。そのため次の経路が成立しました。

password(1要素目で本人確定)
→ ciba_start ×3
→ success_count が 3 に到達 → 認証完了 → 認可コード発行

端末での承認・ciba_poll・userinfo を一度も通さずに2要素目が成立します。

バージョンを上げても直りません

テンプレートはセットアップ時に手動で流し込むもので、起動時に再適用される仕組みはありません。認証ポリシーはテナントごとにデータベースへ保存されているため、本バージョンへ更新してもテンプレートの修正は既存テナントに反映されません。

つまりこの変更で壊れるものは何もありませんが、すでに構築済みのテナントは脆弱な設定のまま残ります。管理APIでポリシーを更新してください。

config/templates/use-cases/ciba-delegation/authentication-policy-oauth.json をベースにテナントを構築している場合、success_conditions を内訳参照に更新してください。

"success_conditions": {
"any_of": [[
{ "path": "$.password-authentication.success_count",
"type": "integer", "operation": "gte", "value": 1 },
{ "path": "$.external-api-authentication.interactions.ciba_start.success_count",
"type": "integer", "operation": "gte", "value": 1 },
{ "path": "$.external-api-authentication.interactions.ciba_poll.success_count",
"type": "integer", "operation": "gte", "value": 1 },
{ "path": "$.external-api-authentication.interactions.userinfo.success_count",
"type": "integer", "operation": "gte", "value": 1 }
]]
}

適用は全インスタンスの更新後に​

この修正ポリシーは v0.13.0 以降でのみ機能します

内訳(interactions.*)は本バージョンで追加されたものです。ローリングデプロイ中(新旧混在)に修正ポリシーを適用すると、そのテナントの認証がすべて完了しなくなります。

旧インスタンスは内訳を復元しないため参照パスが解決せず、条件が満たされないまま in_progress で止まります(詳細は 2.5)。

セキュリティ修正は急いで適用したくなりますが、全インスタンスの更新完了を確認してからポリシーを更新してください。

暫定回避策はありません​

旧バージョンのままでは合計しか参照できず、条件式でこの問題を回避する手段はありません。

利用できる演算子(eq / gte / in など)はいずれも単一のパスの値に対する比較で、合計は1つの数値です。「3種類の interaction を通った」と「1つを3回呼んだ」を合計だけで区別できません。閾値を上げても、その回数だけ ciba_start を呼べば満たされます。

運用上の緩和は、該当ポリシーを無効化する(enabled: false)ことのみです。ただしこれはその認証方式自体を停止することになります。

同様の設定をしていないか点検してください。 external-api-authentication の合計 success_count で条件を書いている構成は、同じ interaction を繰り返すだけで成立します。とくに「複数の interaction を順に通す」ことを意図している場合は該当します。

なお pending を返す ciba_poll は 400(失敗)のため合計には入りません。正常系の 3 は「成功した3段」と一致しており、そこは意図どおりでした。問題は同じ interaction を繰り返せる点にあります。

2.4 影響(機能追加そのもの)​

内訳の追加自体は 🟢 で、既存設定の挙動は変わりません。

対象影響
既存の success_conditions(合計を参照)なし。合計は従来どおりの和
external-api-authentication 以外の認証方式なし。interactions キー自体が出力されません
保存済みの認証トランザクションなし。interactions の無い行は内訳が空として読み込まれます
/v1/authorizations/{id}/authentication-status のレスポンスinteraction_results 配下に interactions キーが増えます

最後の1点のみ、レスポンスを厳密なスキーマで検証しているクライアントは追従が必要です(キーの追加のみ)。

2.5 移行手順とデプロイ順序​

設定変更は不要です。多段フローで各 interaction を必須にしたい場合のみ、success_conditions を内訳参照に書き換えてください。

内訳を参照するポリシーは、全インスタンスの更新後に設定してください

ローリングデプロイ中(新旧インスタンスが混在している状態)で内訳参照のポリシーを設定すると、認証が完了しなくなります。

旧バージョンのインスタンスは認証結果から内訳を復元しません。そのため、

  1. 内訳参照のパス($.external-api-authentication.interactions.step-a.success_count)が解決せず null になる
  2. gte などの数値比較は null に対して false になる(#1646)
  3. 条件が満たされず、認証ステータスが in_progress のまま進まない

さらに旧インスタンスが認証トランザクションを保存し直すと、それまでに記録された内訳が失われます(合計は保持されます)。認証フローが新旧インスタンスをまたぐと、すべての interaction を実行しても条件が揃いません。

エラーにはならず in_progress のまま止まるため、利用者からは原因が分かりません。

推奨手順:

  1. 全インスタンスを更新する
  2. 更新完了後に、内訳参照のポリシーを設定する

内訳を使わない構成(合計のみを参照、または external-api-authentication を使わない)は、混在中も影響を受けません。

認証を通してしまう方向には倒れません

条件が満たされない側(in_progress)に倒れるため、必須の interaction を実行しないまま認証が完了することはありません。混在中の影響は「完了できない」であり、「通ってしまう」ではありません。

2.6 保存済みデータの互換性​

状況挙動
旧バージョンで開始した認証を新バージョンが読む内訳が無いものとして読み込み、以降のステップから内訳を記録
新バージョンで記録した内訳を旧バージョンが読む内訳を無視して読み込む(エラーにはならない)。保存し直すと内訳は失われる
完了済み・保存済みの認証トランザクション影響なし

データ移行・マイグレーションは不要です(interaction_results は JSON カラムで、キーの追加のみ)。

2.7 動作確認​

ケーステスト
1つの interaction を3回呼んでも3段の条件を満たさないe2e/.../usecase/advance/advance-19-external-api-interaction-results.test.js
各 interaction を1回ずつ呼ぶと認証が完了する同上
内訳の集計・ポリシーパス・保存往復・後方互換AuthenticationInteractionBreakdownTest(core unit)
ciba_start を3回呼んでも2要素目が成立しないe2e/.../scenario/application/scenario-13-oauth-ciba-delegation.test.js

2.8 関連​


3. JWT 検証の厳格化(Issue #1776 / #1779 / PR #1526 / #1777 / #1780)​

3.1 iat / nbf に 60 秒の上限がついた(🔴 破壊的)​

FAPI 2.0 対応(PR #1526)で JwtClockSkewValidator を新設し、プロファイルを問わず次の3経路から呼ぶようにしました。

経路呼び出し元
client assertion(private_key_jwt / client_secret_jwt)ClientAuthenticationJwtValidatable
DPoP proofDPoPProofVerifier
Request Object(JAR)RequestObjectVerifyable

iat または nbf が idp-server の時計より 60 秒より先の未来なら拒否します(FAPI 2.0 Security Profile §5.3.2.1-2.13)。

経路拒否時
client assertionClientUnAuthorizedException → invalid_client(401)
Request ObjectRequestObjectInvalidException → invalid_request_object
DPoP proofDPoPProofInvalidException(400)

これまで上限は無く、未来の iat はいくら先でも素通りしていました。

クライアント側の時計が 60 秒以上進んでいると、これまで通っていたリクエストが通らなくなります

FAPI 2.0 テナントに限らず、private_key_jwt / client_secret_jwt / JAR を使う全クライアントが対象です。NTP 同期されていれば影響しません。

同期していないホストからクライアント認証している場合は、アップグレード前に時刻同期を確認してください。エラーは invalid_client として返るため、クライアント側からは「認証情報が誤っている」ようにしか見えません。

過去方向(iat が古い)に制限は入れていません。exp の検証は従来どおりです。

FAPI 条項との関係​

60 秒の上限は FAPI 2.0 由来で、FAPI 1.0 の条項とは縛っている軸が違います。

仕様条項内容方向対象idp-server の実装
FAPI 2.0 SP Final§5.3.2.1-13iat / nbf が未来 0〜10 秒なら受理し、60 秒超は拒否未来JWT 全般JwtClockSkewValidator(v0.13.0 新規・全プロファイル)
FAPI 1.0 Advanced§5.2.2-13request object の exp は nbf から 60 分以内有効期間request object のみFapiAdvanceVerifier(v0.13.0 以前から・FAPI Advance 時のみ)
FAPI 1.0 Advanced§5.2.2-17request object の nbf は過去 60 分以内過去request object のみ同上

FAPI 1.0 Advanced の 2 条項は「request object の古さ・有効期間」を、FAPI 2.0 の条項は「未来方向のずれ」を縛るもので、重複も矛盾もしません。FAPI Advance プロファイルのテナントは、v0.13.0 で既存の 2 条項に加えて未来 60 秒の上限が乗ります。

条文(FAPI 2.0 §5.3.2.1-13)は次のとおりで、10〜60 秒の扱いは実装裁量です。

to accommodate clock offsets, shall accept JWTs with an iat or nbf timestamp between 0 and 10 seconds in the future but shall reject JWTs with an iat or nbf timestamp greater than 60 seconds in the future

idp-server は iat > now + 60秒 を拒否する実装で、ちょうど 60 秒は受理します(裁量の範囲で最も緩い側)。

全プロファイルに掛けたのは意図的です

60 秒も先の iat を打つクライアントは、厳しく弾かれているのではなく単に時計が壊れています。 正常に運用されているクライアントがこの上限に触れることはないため、FAPI 2.0 の条項ではあるものの、プロファイルを問わない既定として採用しました。リプレイの窓を絞る効果もあります(JwtClockSkewValidator の Javadoc に同趣旨を記載)。

したがってこれは仕様適合のための措置ではなく、idp-server の判断で適用範囲を広げたものです。利用者影響の本体はここにあります — FAPI を使っていないテナントも対象になるため、時刻同期されていないホストからクライアント認証している構成だけが影響を受けます。

3.2 登録済みクレームが null / 非文字列でも 500 にならなくなった(🟡)​

JsonWebTokenClaims.hasXxx() がキーの存在(containsKey)だけを見ていたため、値が JSON null のクレームでガードを通過し、直後の getXxx() が null を返して NPE → HTTP 500 になっていました(#1776)。getValue() も値を無条件に (String) へキャストしていたため、数値・配列でも同じ形で落ちました(#1779)。

DPoP proof は proof 自身に埋め込まれた公開鍵で署名検証されるため、攻撃者が自己署名した jti:null / htm:123 の proof で、未認証のまま 500 を発生させられる状態でした。

BeforeAfter
{"jti": null} の DPoP proofNPE → 500DPoPProofInvalidException → 400
{"htm": 123} の DPoP proofClassCastException → 500400
null / 非文字列クレームを含む client assertion・Request Object・外部IdP の id_token500400 / 401

正常な JWT は登録済みクレームに null を持たないため、正規のトラフィックには影響しません。

3.3 Request Object の request / request_uri 混入検知がキーベースになった(🟡)​

JAR (RFC 9101) §4 は "request and request_uri parameters MUST NOT be included in Request Objects" と定めており、含まれていること自体が違反です。従来は値を取り出して「非 null かつ非空」で判定していたため、次が素通りしていました。

Request Object の中身BeforeAfter
{"request": "..."}(文字列)拒否拒否(変更なし)
{"request": ""}素通り拒否
{"request": 123}ClassCastException → 500拒否(invalid_request_object)

エラーメッセージの章番号も JAR Section 6.2 → JAR Section 4 に訂正しています(§6.2 は別内容)。

変わったのは混入検知だけで、パラメータのマージ規則は従来どおりです

JAR は「認可サーバーは request object の中のパラメータだけを使う」(§5 / §6.3)と定めており、これは OIDC Core §6.3.3 の「両側にあれば中を優先してマージする」からの破壊的変更です。v0.13.0 はこの合成規則には手を付けていません。

idp-server の合成規則はプロファイルで分岐します(OAuthRequestContextCreator#selectRequestObjectType)。

プロファイル実装規則
FAPI_ADVANCEFapiAdvanceRequestObjectPatternFactoryJAR 準拠。request object の中だけを使う
それ以外(OAUTH2 / OIDC / FAPI_2_0 / FAPI Baseline)RequestObjectPatternFactoryOIDC Core §6.3.3 準拠。中にあれば優先し、無ければ外側のクエリパラメータにフォールバック

分岐は profile.isFapiAdvance() のみを見ているため、FAPI_2_0 は OIDC Core 側の合成規則になります。FAPI 2.0 Security Profile Final の必須要件は PAR であり、本リポジトリの documentation/requirements/fapi-2.0-requirements.yaml にも request object / JAR の条項はありません。JAR を必須とするのは FAPI 2.0 Message Signing の領分のため、現状の選択が適合上の問題になるかは Message Signing に着手する際に判断します。

3.4 動作確認​

ケーステスト
Nimbus の null クレーム挙動のピン留め・hasXxx() の契約JsonWebTokenClaimsTest / NimbusJoseBehaviorTest(platform)
60 秒超の iat / nbf を拒否・null で NPE を出さないJwtClockSkewValidatorTest(platform)
自己署名 proof(jti:null)が 400 になるDPoPProofNullClaimTest(core)
JAR §4 の混入拒否e2e/src/tests/spec/rfc9101_jar.test.js

4. require_pushed_authorization_requests が実際に強制されるようになった(🔴 破壊的)​

Issue #1736 クラスタD / PR #1741。

AuthorizationServerConfiguration.requirePushedAuthorizationRequests(RFC 9126 §5)は discovery に広告されるだけで、どの verifier からも参照されない dead flag でした。実際の PAR 強制は fapi20_scopes 由来の FAPI 2.0 プロファイル限定で、非 FAPI テナントがフラグを立ててもメタデータどおりには動きませんでした。

RequirePushedAuthorizationRequestVerifier を新設し、OAuthRequestVerifier の extension verifier 列に登録しました。

BeforeAfter
require_pushed_authorization_requests: true のテナントへの直接認可リクエストそのまま処理invalid_request(redirect でエラー返却)
PAR エンドポイント自身へのリクエスト処理処理(変更なし。isAtPushedEndpoint() でスキップ)
false(既定)のテナント処理処理(変更なし)
フラグを立てているテナントは、PAR を経由しないクライアントが弾かれます

既定値が false のため大半のテナントは無影響ですが、過去に「広告だけ」のつもりで true にしたテナントは、この変更で直接認可リクエストが拒否されるようになります。設定を変えていないのに挙動が変わる唯一のケースです。

アップグレード前に該当テナントを確認してください。

-- PostgreSQL
SELECT tenant_id
FROM authorization_server_configuration
WHERE payload::jsonb ->> 'require_pushed_authorization_requests' = 'true';
-- MySQL
SELECT tenant_id
FROM authorization_server_configuration
WHERE JSON_EXTRACT(payload, '$.require_pushed_authorization_requests') = TRUE;

該当した場合の選択肢は 2 つです。

  1. 意図どおり(PAR を必須にしたかった)→ クライアントを PAR 経由に移行してからアップグレードする
  2. 意図と違う(広告だけのつもりだった)→ アップグレード前にフラグを false に戻す

なお、6.1 の修正が入るまでは、管理APIの GET → PUT 往復でこのフラグが true → false に戻っていました。往復で黙って解除されていたテナントは、v0.13.0 で往復しても解除されなくなります。この2つは組み合わせて効きます。

ケーステスト
discovery が true を広告 / 直接リクエストが invalid_request / PAR push は受理e2e/src/tests/spec/rfc9126_par_required.test.js(専用テナント require-par-tenant)
shouldVerify マトリクスと拒否・許可RequirePushedAuthorizationRequestVerifierTest(core unit)

5. 外部ソースで解決したユーザーの扱いが変わった(Issue #1767 / #1772 / #1773 / #1786 / #1792 / PR #1784 / #1785 / #1788 / #1793 / #1794 / #1795)​

user_resolve とマッピング周辺の 6 本の修正です。いずれも設定を変えずに挙動が変わります。まとめて記載します。

5.1 解決したユーザーが既存ユーザーベースになった(🟡)​

外部ソースで1要素目のユーザーを解決すると、認証トランザクションのユーザーがマッピングの出力 + sub / status だけになっていました。認可グラントはそれをスナップショットするため、既存ユーザーが持っていた属性はそのセッションのトークンに載りませんでした。

出力Before
ID Token / アクセストークン❌ マッピングが出した属性しか載らない
UserInfo✅ 正しい(UserinfoHandler が DB から取り直すため)
DB のユーザーレコード✅ 正しい

とくに verified_claims は User#updateWith の patch 対象外なので、マッピングから組み立てたユーザーには構造上絶対に載りません。身元確認済みのユーザーがフェデレーションでログインすると、そのセッションのトークンから verified_claims が落ちていました。UserInfo を見ると正しいため、気づきにくい失敗でした。

4 経路すべてを ResolvedUserCreator(existingUser.enrichWith(mapped) + status 固定)に集約しました。

経路実装
フェデレーションOidcFederationInteractor#resolveUser
外部API認証ExternalApiAuthenticationInteractor#resolveUser
外部パスワード認証PasswordAuthenticationInteractor#resolveUserFromExternalAuth
外部トークン認証ExternalTokenAuthenticationInteractor

そのセッションのトークンに載るクレームが増えます。 既存ユーザーが持っていて外部ソースが返さない属性(verified_claims / roles / permissions / authentication_devices 等)が ID Token・アクセストークンに現れるようになります。トークンのサイズが増えるほか、クレーム集合を固定前提で扱っているクライアントは確認が必要です。

status は既存値で固定します(外部ソースが LOCKED を REGISTERED に復活させられないため)。sub / provider_id / external_user_id も updateWith の immutable 扱いで安定します。

既知のトレードオフ: 巻き戻りの窓

1要素目で取った既存ユーザーのスナップショットが authorize 時の registerOrUpdate まで持ち回られるため、認証中に管理APIでそのユーザーを PATCH すると巻き戻る窓ができます。窓は認証フローの所要時間ぶんで、次の管理操作で自己修復します。

修正前も「マッピングが出すフィールド」については同じ挙動でしたが、修正後は updateWith が patch から取る全フィールド(新たに roles / authentication_devices / assigned_tenants)が対象になります。とくに roles は権限状態なので、単なる属性の巻き戻りより意味が重くなります。

5.2 custom_properties がキー単位マージになった(🟡)​

User#updateWith は「patch 側に1つでもキーがあれば map ごと差し替える」実装で、custom_properties は federation / 外部API認証 / 2要素目 / 身元確認 が書き込む共有のフラットなキー集合です。そのため後から通った方式が他方式のキーを黙って消していました。

User#enrichWith を新設し、認証フローの経路をそちらへ寄せました。

BeforeAfter
ルールが値を生成したキー上書き上書き
ルールが値を生成しなかったキーnull で上書き(PR #1788 で修正)既存値を保持
ルールが宣言していないキーmap ごと消失保持

User#updateWith は変更していません。管理API の PATCH は従来どおり全置換です(「認証フローは育てる / 管理APIは指示どおり」で意味を分けています)。

他方式が入れたキーが消えなくなるため、custom_properties を読む側が見るキー集合が増えます。「消える」ことを前提にしていた構成はありえないため、実質は改善方向の変更です。

5.3 $.request_attributes.* が解決するようになった(🟡)​

認証の HTTP executor 2 つが RequestAttributes を toMap() せずにマッピングコンテキストへ入れていたため、$.request_attributes.ip_address / .user_agent が常に null に解決されていました。身元確認・イベント系(15箇所以上)は toMap() 済みで、この2箇所だけが逸脱していました。

箇所BeforeAfter
HttpRequestAuthenticationExecutor生で put(解決せず null).toMap()
HttpRequestsAuthenticationExecutor生で put(解決せず null).toMap()

これまで null だったパスが値を返すようになります。 external-api-authentication の body_mapping_rules / header_mapping_rules に $.request_attributes.* を書いている構成は、外部サービスへ送るリクエストの中身が変わります。「書いたが効いていなかった」ものが効き始めるため、外部サービス側が想定していない値を受け取る可能性があります。

公開範囲は身元確認とパリティで、headers を含む全キーです。headers には Authorization / Cookie が含まれるため、外部サービスへ横流ししないよう設定を確認してください。

あわせて RequestAttributes.toMap() を null 安全化しました(jsonNodeWrapper が null なら空 Map。挙動変化は「NPE → 空 Map」だけ)。

5.4 user_resolve のマッピングから $.user を参照できるようになった(🟡)​

user_resolve の user_mapping_rules に $.user を書いても、例外にもログにもならず黙って nullになっていました。body_mapping_rules では #1439 で使えるため、同じ記法を書いて「動いているように見えて効いていない」状態になりやすい形でした。

1要素目は、ユーザーを探すキー(provider_id / external_user_id)自体がマッピングの産物であるため、二段マッピングにしています。

① user_mapping_rules を評価 → 検索キーを得る
② 既存ユーザーを検索し、mappingSource に "user" を注入
③ user_mapping_rules を再評価 → こちらが採用される

1要素目では user_mapping_rules が 2 回評価されます。 マッピング関数はすべて副作用のない値変換ですが、now / uuid4 / random_string は①と③で異なる値を返します。採用されるのは③の結果で、新規ユーザー分岐では検索キーを①の値にピン留めしています(PR #1795)。これらの関数を user_mapping_rules に書いている構成は、生成される値が「検索に使った値」と一致しない点に注意してください。

http_requests executor にも $.user を注入し、単数 http_request との非対称(同じマッピングルールが executor の選択によって効いたり効かなかったりする)を解消しました。

2要素目の書き込み側 allowlist(SECOND_FACTOR_ENRICHABLE_CLAIMS、CWE-287 / #1497)は不変で、$.user は読み取りにしか使えません。verified_claims / hashed_password / credentials は投影から除外されています。

5.5 解決しない JSONPath 引数が warn に出るようになった(⚙️)​

関数の args に書いた $. 参照が解決しないと、関数はそれを吸収して動き続けていました。とくに merge は source が null だと入力をそのまま返すため、設定上は「既存値に積み増す」と読めるルールが実際には「今回の値で置き換える」になっていました。

未解決時に function / arg / path / to を warn へ出します。ルール自身の from は対象外です(パス不在は通常の分岐で、意図的に解決させないイディオムが使われているため)。

ログ量が増えます。既存設定に未解決の args があれば、アップグレード後に warn が出始めます。

5.6 動作確認​

ケーステスト
外部ソースが返さない属性がトークンに載る(4経路)e2e/.../usecase/advance/advance-17-external-1st-factor-token-claims.test.js / advance-16-external-token-response-mapping.test.js
status 固定・識別子の不変・custom_properties のキー単位マージResolvedUserCreatorTest(core unit・6ケース)
方式をまたいだ custom_properties のマージe2e/.../scenario/application/scenario-14-custom-properties-merge.test.js
生成されなかったキーで既存値が消えないUserEnrichWithTest(core unit)
$.request_attributes.* の解決e2e/.../integration/authentication-interactors/...-request-attributes.test.js
user_resolve から $.user を参照e2e/.../scenario/application/scenario-15-user-resolve-existing-user.test.js
未解決 args の warnMappingRuleObjectMapperArgsResolutionTest

5.7 関連​


6. 管理APIの GET が返す表現と更新セマンティクス(Issue #1742 / #1743 / #1745 / #1755 / #1762 / #1729 / #1845 / PR #1746 / #1748 / #1749 / #1750 / #1764 / #1765 / #1846)​

管理APIの update は全置換です。したがって次の不変条件が要ります。

GET が返す表現は、update が読み戻せる全フィールドを含まなければならない。

v0.13.0 ではこれが守られていなかった箇所を横断で塞ぎました。GET のレスポンス構造が変わるため、管理APIを自動化している利用者は影響を受けます。

6.1 authorization-server の GET が全フィールドを返すようになった(🔒 要点検)​

GET → 編集 → PUT の read-modify-write で、GET が出力していなかったフィールドが黙って失われていました。差分も同じ toMap() 同士の比較で計算するため、dry_run の diff にも現れませんでした。

実測(PR #1764)修正前
GET レスポンスの jwksキーごと欠落
GET 結果を PUT した dry_run の diff{}
往復後の /{tenant}/v1/jwks500

AuthorizationServerConfiguration#toMap() に jwks / require_pushed_authorization_requests / revocation_endpoint(+ auth_methods, auth_signing_alg)/ introspection_endpoint(同)/ authorization_encryption_alg_values_supported / authorization_encryption_enc_values_supported / authorization_details_types_supported を追加しました。

管理APIのレスポンスと監査ログに署名鍵の秘密部が入ります

jwks は秘密鍵を含む設定値です。修正により次の 2 箇所に現れるようになります。

出力先経路
authorization-server 管理APIの GET / PUT レスポンスAuthorizationServerFindService / AuthorizationServerUpdateService
監査ログの before_payload / after_payloadAuthorizationServerManagementContext#beforePayload() / afterPayload()

discovery(/.well-known/openid-configuration)と JWKS エンドポイントには影響しません。 discovery は toMap() を使わず独自にマップを組んでおり、JWKS エンドポイントは JwkParser.parsePublicKeys() で公開鍵だけを返します。

監査ログには期限切れ削除の仕組みがありません。 oauth_token / authorization_request / authorization_code_grant / sso_session / authentication_transaction には削除の実装がありますが、audit_log を削除する経路はコード上に存在しません。一度書かれた秘密鍵はテーブルに残り続けます。

次を点検してください。

  • authorization-server の管理API権限を持つ operator の範囲
  • 監査ログの保管先・アクセス権・ログ転送先(外部 SIEM 等へ流している場合は転送先にも秘密鍵が渡ります)

credential_issuer_metadata は対象外です(VC のモデルが暫定で、保存されているメタデータの全メンバーを保持していないため。出力すると「モデルが知らない部分を落とす往復」を成立したように見せてしまいます)。

6.2 federation 設定の更新が全置換になった(🔴 破壊的)​

federation 設定は、経路ごとに enabled の扱いが食い違っていました。

経路Before原因
updateenabled=false を送っても true に戻る4引数コンストラクタで再構築
read (GET)常に trueSELECT が enabled 列を取っていない
createenabled=false を無視手動抽出 + 4引数コンストラクタ
listdisabled が常に消えるselectList が AND enabled = true をハードコード

ClientConfiguration / ClientUpdateService に揃えて、update / create を jsonConverter.read(map, ...) による全置換にしました。

部分更新していると、送らなかったフィールドが消えます

これまで update は request の一部フィールドしか見ておらず、結果として「送らなかったフィールドは残る」ように見えていました。全置換になったことで、ボディに含めなかったフィールドは消えます。

federation 設定を PUT する自動化は、GET の結果をベースに全フィールドを送る形に直してください。sso_provider だけは例外で、リクエストに非空の値が無ければ既存値を維持します(6.3)。

一覧(list)は 既定で全件を返し、enabled はクエリフィルタになりました(?enabled=true で絞れます)。管理者がデータを確認できるようにするためで、client と同型です。

一覧のレスポンス件数が増えます。 disabled の設定を持つテナントで、一覧を件数前提・全件有効前提で扱っている自動化は追従が必要です。

ランタイムの federation フローが使う selectOne の includeDisabled は不変です(「enabled のみ取得」に依存しているため)。

6.3 federation の sso_provider が GET で返るようになった(🟡)​

sso_provider は設定行の引き当てキーです(専用カラム sso_provider VARCHAR(255) NOT NULL、UNIQUE(tenant_id, type, sso_provider))。federation リクエスト時に WHERE sso_provider = ? で設定行を fetch します。

BeforeAfter
GET レスポンスの sso_provider返らない返る
部分更新で sso_provider を省略空文字に潰れて永続化され、以降 config not found で federation が破綻既存値を維持

update SQL は毎回 sso_provider = ? を書くため、空文字が NOT NULL 制約を通過して保存され、外部IdP連携が動かなくなっていました。

6.4 テナント更新で name が反映されるようになった(🟡)​

TenantUpdateService.updateTenant() が after を組む際、request の name ではなく **before.name()(既存 name)**を渡していました。before/after が一致するため diff も空でした。

これまで無視されていた name が反映されます。 テナント更新API に「変えるつもりのない name」をボディへ含めていた自動化は、その値でテナント名が上書きされます。GET の結果をそのまま PUT している場合は同じ値なので影響しません。

identifier / type / authorization_provider / main_organization_identifier は不変項目として before を維持します。

6.5 認証ポリシー一覧が policies を返すようになった(🟡)​

AuthenticationPolicyConfigurationQueryDataSource#findList だけが、行 Map({id, flow, payload})をそのままデシリアライズしていたため、policies が常に空配列でした。id と flow は行のカラムでもあるためマップされてしまい、レスポンスが一見それらしく見えるので壊れていることが分かりにくい形でした。

実測(PR #1765・limit=3)修正前修正後
policies の件数0 / 0 / 02 / 1 / 1

detail(find / get)とランタイムは元から正しく、DB のデータも正常でした。selectList を持つ QueryDataSource 19 本を横断で確認し、同型は本箇所のみでした。

一覧のレスポンスサイズが増えます。 これまで空だった policies に実体が入ります。

6.6 grant 管理APIのレスポンスにフィールドが追加された(🟢)​

AuthorizationGranted.toMap() に追加しました。読み取り経路で DB から復元済みのため、追加クエリは発生しません。

フィールド出力条件
grant_type常時(OpenAPI で required)
authorization_details / id_token_claims / userinfo_claims / custom_properties存在時のみ

一覧・詳細とも AuthorizationGranted.toMap() を通るため、両方に効きます。

6.7 fapi20_scopes が GET で返るようになった(🔒 要点検)​

AuthorizationServerExtensionConfiguration#toMap() に fapi20_scopes が無く、6.1 の jwks と同じ経路で往復のたびに失われていました(PR #1846)。fapi_baseline_scopes / fapi_advance_scopes は出力されていたため、FAPI 2.0 だけが欠けている状態でした。

該当するのは、GET → 編集 → PUT を自分で書いている構成だけです。

更新のしかた該当
設定ファイルを正として PUT する(同梱スクリプトのやり方。config/scripts/update-admin-tenant.sh:147 は $CONFIG_FILE から送り、GET は存在確認にしか使わない)❌ 該当しない。fapi20_scopes は自分のファイルにあるのでそのまま送られる
手で GET して 1 フィールドだけ直して PUT する✅ 該当する
GET の結果をベースに PUT する自動化を書いている✅ 該当する

サーバ側は往復しません。AuthorizationServerUpdateService:77 は jsonConverter.read(request.toMap(), ...) でリクエストボディだけから構成を組み立てており、before.toMap() は diff 表示にしか使いません。read-modify-write をする管理コンソールもありません。

以下は、該当する場合に何が起きるかの説明です。

失われると FAPI 2.0 固有の検証が掛からなくなります

fapi20_scopes は「このリクエストに FAPI 2.0 プロファイルを適用するか」の判定に使われます。

// OAuthRequestContextCreator:46-47
if (authorizationServerConfiguration.hasFapi20Scope(filteredScopes)) {
return AuthorizationProfile.FAPI_2_0;
}

プロファイルは上から順に判定され、fapi20_scopes が空だとひとつ下の段に落ちます(analyze():46-59)。

hasFapi20Scope()      → FAPI_2_0
hasFapiAdvanceScope() → FAPI_ADVANCE
hasFapiBaselineScope()→ FAPI_BASELINE
scopes に openid → OIDC
それ以外 → OAUTH2

OAuthRequestVerifier:79 が baseVerifiers.get(context.profile()) で Verifier を選ぶため、FapiSecurity20Verifier は実行されません。無検証になるのではなく、落ちた先のプロファイルとして検証されます。 掛からなくなるのは FAPI 2.0 固有の次の要件です。

検証根拠
PAR 必須FAPI 2.0 §5.3.2.2.3
response_type=code 限定§5.3.2.2
PKCE S256 必須§5.3.2.2-2.5
redirect_uri の https 必須§5.3.2.2-2.8
client_secret_post / client_secret_basic / client_secret_jwt の禁止§5.3.2.2
public client の禁止同上
sender-constraining(mTLS または DPoP)必須同上

エラーにはならず、落ちた先のプロファイルとして正常に処理されます。 しかも FAPI 2.0 クライアントは PAR も PKCE S256 も sender-constraining も自分から送るため、正常系は通ったままです。差が出るのは「本来 400 になるべきリクエストが通る」ときだけなので、機能テストでは捕まりません。

往復してしまった環境では、更新しても復元されません

本修正は「これから GET が返すようにする」ものです。すでに GET → 編集 → PUT を経て fapi20_scopes が空になっている設定は、そのまま残ります。

上の表で「該当する」に当てはまり、かつ FAPI 2.0 を使っているテナントは、現在の値を確認してください。

curl -s -H "Authorization: Bearer ${TOKEN}" \
"${BASE_URL}/v1/management/tenants/${TENANT_ID}/authorization-server" \
| jq '.extension.fapi20_scopes'

空配列 / null が返り、かつ FAPI 2.0 を適用する想定のテナントであれば、再設定が必要です。

6.8 移行手順​

設定変更・データ移行は不要です。管理APIを自動化している場合のみ、次を確認してください。

対象確認
federation 設定を PUT しているGET の結果をベースに全フィールドを送る形にする(6.2)
federation 設定を一覧しているdisabled が混ざる。必要なら ?enabled=true
テナントを PUT しているボディの name がそのまま反映される
レスポンスを厳密なスキーマで検証しているauthorization-server(jwks 他)/ federation(sso_provider)/ authentication-policy(policies)/ grant(grant_type 他)でキーが増える
監査ログを外部へ転送しているauthorization-server の before/after に秘密鍵が入る(6.1)
GET の結果をベースに authorization-server を PUT している(設定ファイルから送っている場合は不要)FAPI 2.0 を使っているなら fapi20_scopes が空になっていないか確認する。空なら再設定(6.7)

6.9 動作確認​

ケーステスト
GET → PUT 往復で全フィールドが保たれるAuthorizationServerConfigurationRoundTripTest / ManagementRepresentationCompletenessTest
federation の enabled・全置換・一覧フィルタorganization_federation_config_management.test.js / FederationConfigCreationServiceTest / FederationConfigUpdateServiceTest
sso_provider の保全FederationConfigurationTest / 同 E2E
テナント name の反映organization_tenant_management.test.js / ..._structured.test.js
一覧の policies の中身organization_authentication_policy_config_management.test.js
grant のフィールド出力AuthorizationGrantedTest / organization_grant_management.test.js
fapi20_scopes の往復保全ManagementRepresentationCompletenessTest / organization_authorization_server_management.test.js

7. ui_locales / acr_values が要求順を保持するようになった(Issue #1801 / #1811 / PR #1809)​

UiLocales / AcrValues が値を HashSet に入れていたため、空白区切りの優先順位が失われていました。どちらも仕様上は順序に意味があります(ui_locales は "ordered by preference"、acr_values は "in order of preference")。LinkedHashSet に変更しています。

BeforeAfter
ui_locales=fr-CA fr en順序不定(en fr-CA fr などになる)fr-CA fr en
acr_values=... silver ... bronze順序不定要求順

7.1 影響​

acr_values の消費側の挙動は変わりません。 参照は 3 箇所とも contains() による membership 判定で、順序を見ていません。

一方 toStringValues() で保存している箇所が 5 つあり(認可リクエスト / CIBA バックチャネル認証リクエスト / 認証トランザクション、いずれも両DB)、DB に残る値の順序が変わります。

保存済みの行は変わりません。v0.13.0 以降に作られた行だけが要求順になります。認可リクエストや認証トランザクションの acr_values / ui_locales を読んで順序に依存した処理をしている場合は確認してください。

ui_locales は空要素も落とすようになりました(区切りの重複で空タグが混ざると、画面が解決すべき locale として扱ってしまうため)。acr_values の空要素の扱いは変えていません。

7.2 ui_locales が画面まで届くようになった(🟢)​

これまで ui_locales は AuthorizationRequest まで運ばれていましたが、そこから先で誰も読んでいませんでした。画面へ渡す経路も 3 つとも塞がっていました。

経路形
view-data配列 ["fr-CA", "fr", "en"]
サインイン画面へのリダイレクトURL空白区切り ?ui_locales=fr-CA+fr+en

ui_locales_supported によるフィルタはしません(仕様上サポート外の locale でもエラーにはせず、どのバンドルを持つかを知っているのは画面側のため)。

7.3 discovery に ui_locales_supported が出るようになった(🟢)​

ui_locales_supported は全テナントテンプレートに書かれ、OpenAPI にも記載されているのに AuthorizationServerConfiguration にフィールドが無く、Jackson が読み捨てていました(設定は登録できるのに discovery に出ない)。

discovery のレスポンスにキーが増えます。 ui_locales_supported を設定済みのテナントは、アップグレード後に広告され始めます。

claims_locales_supported は同じ形ですが、claims_locales リクエストパラメータ自体が未実装のため対象外にしています(無い機能を広告することになるため)。

7.4 動作確認​

ケーステスト
順序保持・空要素の除去UiLocalesTest / AcrValuesTest(core unit)
discovery の ui_locales_supportedServerConfigurationResponseCreatorTest
サインインURL・view-data への伝播e2e/.../usecase/advance/advance-20-ui-locales.test.js

8. FAPI 2.0 Security Profile Final と DPoP の追加(Issue #1736 / PR #1526 / #1737 / #1738 / #1739 / #1741)​

AuthorizationProfile.FAPI_2_0 と RFC 9449 DPoP を新設しました。プロファイル自体が新規のため、既存テナントは fapi20_scopes を設定するまで影響を受けません。 全プロファイルに効く変更は 3.1 と 4 に分けて記載しています。

8.1 追加された強制(FAPI 2.0 プロファイル時のみ)​

要件実装
PAR 必須・直接認可リクエスト拒否 / response_type=code のみ / PKCE S256 必須 / HTTPS redirect_uriFapiSecurity20Verifier
public client 拒否・client_secret_* 拒否FapiSecurity20Verifier / AuthorizationCodeGrantFapi20Verifier
sender-constrained access token(mTLS / DPoP)FapiSecurity20Verifier
client assertion の aud は AS issuer の string のみAuthorizationCodeGrantFapi20Verifier
client assertion / DPoP proof の alg は PS* / ES* / EdDSA のみ(§5.4)AuthorizationCodeGrantFapi20Verifier / DPoPProofVerifier.verifyFapiSigningAlgorithm
Authorization Code Binding (dpop_jkt, RFC 9449 §10.1)AuthorizationCodeGrantService

DPoP proof の alg 制限は、テナントが dpop_signing_alg_values_supported を未設定にしても RS256 等を拒否します(設定リストの上書きではなく検証で弾く形)。

refresh の §5.4 は対象外です。 AuthorizationGrant に profile が保存されておらず、refresh には profile dispatch verifier もないためです。影響は「RSA 鍵バインドの FAPI 2.0 トークンが refresh 時に RS256 へダウングレードする」edge のみで、EC 鍵なら同じ ES256 に固定されます。

8.2 全テナントに効く DPoP 周辺の変更​

変更影響
/userinfo が 401 時に WWW-Authenticate チャレンジを返すようになった(RFC 9449 §7.1)🟡 Bearer を使う既存クライアントも対象。401 応答にヘッダーが増える
/me の DPoP エラーが invalid_dpop_proof → invalid_token(invalid_dpop_proof はトークンエンドポイント専用)⚙️ DPoP 自体が v0.13.0 の新機能のため実質影響なし
WWW-Authenticate の error_description を RFC 7235 quoted-string 用にエスケープ(制御文字除去)⚙️ ヘッダーインジェクション対策
X-Forwarded-Proto / X-Forwarded-Host をカンマ区切りの先頭値で取るようになった🟡 多段プロキシ(CloudFront/API Gateway → ALB、nginx 多段)で htu が破損して DPoP が全拒否になるのを解消
htu の信頼境界

X-Forwarded-Proto / X-Forwarded-Host は受信値をそのまま使います。htu の完全性は「idp-server が信頼イングレス(reverse proxy / API Gateway + ALB)経由でのみ到達可能」というネットワーク隔離に依存します。

アプリ層の source-IP allowlist は意図的に入れていません。クラウド構成で idp-server が見る即時ピアは動的な内部アドレス(VPC Link / 内部ALB)で、allowlist で塞ぐと DPoP 検証が全面的に壊れる可用性リスクのほうが大きいためです。trusted_proxies 設定は X-Forwarded-For(クライアントIP解決)のゲートで、htu の再構成は保護しません。

idp-server を信頼できないネットワークへ直接公開しないでください。

DPoPProofVerifier.verifyHtu は ポート比較を行います(従来 scheme / host / path のみ)。UriWrapper.equalsPort はデフォルトポート正規化済みなので https://h/p と https://h:443/p は一致し、https://h:8443/p は不一致です。非標準ポートで公開している場合は、クライアントが送る htu と一致するか確認してください。

8.3 DBマイグレーション(必須)​

DPoP のために列を 2 本追加します。PostgreSQL / MySQL 両方に用意しています。

バージョン内容
V0_13_0_1__dpop_jwk_thumbprintoauth_token に jwk_thumbprint VARCHAR(64)
V0_13_0_2__authorization_request_dpop_jktauthorization_request に dpop_jkt VARCHAR(255)

いずれも nullable な列の追加のみ(additive-nullable)で、既定値の指定もバックフィルもありません。PostgreSQL 11 以降は既定値なしの列追加がカタログ更新だけで済むため、oauth_token のような大きいテーブルでもテーブル書き換えは発生しません。MySQL は ALGORITHM=INSTANT の適用条件(列を末尾に追加する等)を満たすかを事前に確認してください。

旧アプリ × 新スキーマ

追加は additive-nullable で、INSERT は explicit-column です。したがって旧バージョンのアプリが新スキーマに対して動作します(ローリングデプロイ可)。

MySQL 側に IF NOT EXISTS が無いのは意図的です(ADD COLUMN IF NOT EXISTS は MariaDB 固有で MySQL 8.0 では構文エラーになります)。冪等性は Flyway のバージョン管理で担保しています。

8.4 その他​

docker/nginx/nginx.conf の全 server ブロックで、BCP195 推奨 cipher のみ・TLS 1.3 ciphersuites を AES-GCM / CHACHA20-POLY1305 に制限しました。同梱の nginx をそのまま使っている環境のみ対象で、独自のロードバランサ / リバースプロキシを前段に置いている場合は影響しません。

8.5 動作確認​

ケーステスト
DPoP 全般(RFC 9449)e2e/src/tests/spec/rfc9449_dpop.test.js / ciba_dpop.test.js
FAPI 2.0 プロファイルe2e/src/tests/spec/fapi2_0_security_profile*・OIDF Conformance Suite fapi2-security-profile-final
DPoP proof の alg 制限(RS256 拒否 / ES256 許可)DPoPProofVerifierTest
X-Forwarded-* のカンマ先頭値・Host 欠落フォールバックParameterTransformableTest
WWW-Authenticate のエスケープ・スキーム判定・401限定SecurityHeaderConfigurableTest

9. 認可画面と federation テンプレートの既定変更(Issue #1728 / #1759 / PR #1747 / #1760)​

いずれもテンプレート・サンプル設定の変更です。テンプレートはセットアップ時に手動で流し込むもので、起動時に再適用される仕組みはありません。本バージョンへ更新しても既存テナントには反映されません。

対象BeforeAfter
e2e test-tenant / admin テナントの ui_confighttp://localhost:3000 + /signin/fido2/ / 未定義(同梱静的ページにフォールバック)https://auth.local.test(${UI_BASE_URL})+ /signin/
admin テナントの cookie_same_siteNoneLax
cors_config.allow_originsauth.local.test 無しauth.local.test / api.local.test を追加
login-social テンプレートの federation redirect_uriバックエンド callback API(@PostMapping)${UI_BASE_URL}/signin/sso-callback/
既存テナントは手動での確認が必要です

SameSite=None は Secure 必須のため、use_secure_cookie: false との組み合わせではブラウザが AUTH_SESSION Cookie の保存自体を拒否し、認可フローが auth_session_mismatch で必ず失敗します。admin テナントをテンプレートから構築している場合は cookie_same_site を確認してください。

federation の redirect_uri がバックエンドの callback API を指していると、外部IdP(Google 等)からの GET リダイレクトが 405 になります。正しい着地先は app-view の sso-callback 画面で、そこが GET を受けて state から tenantId を復号し、バックエンドへ POST します。E2E はモック経由のため露見していませんでした。

config/scripts/update-admin-tenant.sh を追加してあります。テンプレートから再生成して tenant / authorization-server / client を PUT します(-d true で差分のみ確認)。

./config/scripts/update-admin-tenant.sh -d true   # 差分だけ確認
./config/scripts/update-admin-tenant.sh # 反映

このスクリプトはボディを常にテンプレート全体から組み立てます。authorization-server を GET → 編集 → PUT する方式にしないでください(6.1 の修正以前は署名鍵が消えました)。


10. 同意画面で配列クレームの要素を選べるようになった(Issue #1816 / PR #1817)​

denied_claims はクレームを丸ごとしか落とせないため、ユーザーが複数持つもの(口座・カード・契約)を格納した custom_properties は all-or-nothing でした。claims:accounts に同意すると全口座が渡ります。End-User が選んだ要素だけをグラントに載せられるようにしました。

10.1 追加されたもの(🟢)​

経路追加
view-data のレスポンスclaim_values — 選択候補。claims:* スコープが解放する custom_properties のうち配列のものだけ
/authorize のリクエストボディgranted_claim_values — 許可した要素。クレーム名をキー、値は要素の配列

絞る方向にしか働きません。 ユーザーが実際に保有する値との積集合だけが残るため、ボディに保有していない値を書いてもクレームには入りません。

キーを送らなければ従来どおり全要素が共有されるため、既存の構成の挙動は変わりません。

10.2 view-data のレスポンスにキーが増える(🟡)​

claim_values が増えます。返るのは次をすべて満たすときだけです。

  • 認証トランザクションがユーザーを解決済み(認証前は返りません。認可リクエストIDを取得しただけの相手にユーザー属性は渡りません)
  • authorization_server.extension.custom_claims_scope_mapping が有効
  • claims:* スコープに対応する custom_properties が空でない配列

レスポンスを厳密なスキーマで検証しているクライアントは追従が必要です(キーの追加のみ)。

10.3 granted_claim_values は許可リストです​

列挙した要素はそのグラントに固定されます

granted_claim_values は「これらに限定する」という意思表示で、列挙したリストはグラントが生きているあいだ保持されます。同意後にユーザーの保有が増えても、列挙されていない要素は解放されません(新しい要素を解放するには再同意が必要です)。

全要素を列挙することは「制限しない」ではありません。 「いま保有している要素に限定する」になります。制限しないつもりであればキー自体を送らないでください。

同梱の同意画面(app-view)は、ユーザーが1つ以上外したクレームについてのみ送ります。独自の同意画面を実装している場合は同じ扱いにしてください。

丸ごとの拒否は denied_scopes で claims:* スコープを外すことで表現します。denied_claims は custom property のクレームを止めません — クレーム発行はグラントのスコープを見るためです。

10.4 保存とローリングデプロイ​

スキーマ変更・データ移行はありません。選択は「絞った結果」ではなく「絞る決定」としてグラントに保存され、既存のクレーム名 TEXT カラムに gcv:<base64url(JSON)> のセンチネルとして相乗りします(2 と同じく #1628 の RequestedVerifiedClaims の方式)。

決定を保存する形にしたのは、UserInfo がアクセストークンの sub からリポジトリでユーザーを引き直すためです。グラントが持つユーザーのスナップショットを絞る方式では UserInfo に届かず、同意画面で1口座だけ選んでも同じアクセストークンで UserInfo を叩けば全口座が取れてしまいます。

混在中は絞られない側に倒れます

クレーム発行は既知のクレーム名のマッチであってトークン集合の列挙ではないため、旧バージョンのインスタンスは未知の gcv: トークンを無視します。ローリングデプロイ中、新インスタンスが記録した選択は旧インスタンスが発行するトークンでは効かず、全要素が解放されます。

2.5 の内訳参照ポリシーとは倒れる向きが逆です(あちらは認証が完了しない側)。要素単位の同意を有効にするのは全インスタンスの更新完了後にしてください。

10.5 制約​

  • オブジェクト要素は全体一致です。フィールドの集合で比較するため順序は問いませんが、{"id": "card-1"} のような識別子だけの部分指定では選択できません
  • 対象は custom_properties の配列のみです。roles / permissions / assigned_tenants も claims:* で解放されますが、サーバーが決めるものなので対象外です
  • 全要素を拒否するとクレームごと落ちます(null や空配列ではなくキーが消えます。丸ごと拒否と同じ結果、OIDC Core §5.3.2 / #1699)

10.6 動作確認​

ケーステスト
文字列配列・オブジェクト配列 × アクセストークン / ID Token / UserInfoe2e/.../usecase/advance/advance-22-claim-value-selection.test.js
認可エンドポイントが直接発行する ID Token(hybrid)同上
同意後に増えた要素が解放されない(許可リストの固定)同上
denied_claims に同名を指定しても選択が維持される同上
センチネル往復・要素一致の正規化・要素を剪定しないことGrantedClaimValuesTest / GrantClaimsSelectionTest(core unit)

10.7 関連​

  • documentation/openapi/swagger-authentication-interaction-ja.yaml — granted_claim_values(リクエスト)/ claim_values(view-data)
  • config/templates/use-cases/login-password-only/EXPERIMENTS-claim-value-selection.md — 手元での確認手順

11. redirect_uri の完全一致照合フラグを追加(Issue #1835 / PR #1837)​

このリリースでは挙動は変わりません。 既定 OFF のフラグを追加しただけです。次のメジャーで既定を反転する予定なので、その前に自テナントが影響を受けるかを確認できるようにするのが目的です。

11.1 仕様の変遷​

RFC 6749 §3.1.2.3 は redirect_uri の照合を「RFC 3986 §6 で定義された方法」と規定しており、構文ベース正規化(§6.2.2)も許容範囲でした。RFC 9700(BCP 240、2025年1月、RFC 6749 / 6750 / 6819 を更新)は §2.1 でこれを狭めています。

When comparing client redirection URIs against pre-registered URIs, authorization servers MUST utilize exact string matching except for port numbers in localhost redirection URIs of native apps

理由は §4.1.3 に「パターンマッチングを正しく実装・運用することの複雑さが、そのままセキュリティ問題を招く」と述べられています。

11.2 現状の照合(フラグ OFF)​

プロファイルによって照合が異なります。

プロファイル照合
OAuth 2.0完全一致 または構文正規化一致
OIDC完全一致のみ
FAPI Baseline / Advance完全一致のみ

プロファイルは要求スコープで決まるため、同じクライアント・同じ登録 URI でも openid を外すと緩い照合になります。RFC 6749 / RFC 9700 / OIDC Core はいずれも照合方法をクライアント登録の性質で決めており、リクエスト内容で切り替える規定はありません。

OAuth 2.0 プロファイルで通ってしまう差:

差例RFC 3986 の正規化か
scheme / host の大文字小文字HTTPS://APP.EXAMPLE.COM/cb○(§6.2.2)
既定ポートの明示https://app.example.com:443/cb○(§6.2.3)
クエリの付加https://app.example.com/cb?foo=bar✗

3つ目は正規化ではなく、正規化段がクエリを比較していないために通っているものです。

11.3 緊急度​

低い。 上記のいずれも宛先ホストを変えません。scheme / host の大文字小文字は RFC 3986 上も DNS 上も同一、既定ポートの省略も同一エンドポイント、クエリ未比較も宛先は変わりません。オープンリダイレクトや認可コード漏洩には直結せず、RFC 6749 の時点では選べる比較方法の1つでした。後段仕様への追随と挙動の一貫性として扱っています。

11.4 有効化する場合​

{
"extension": {
"redirect_uri_exact_match_required": true
}
}

authorization_server.extension に置きます。true にすると全プロファイルで完全一致に揃います。RFC 8252 §7.3 の native app + loopback のポート無視は、true でも維持されます(RFC 9700 §2.1 自身が唯一の例外として認めているため)。

照合失敗はクライアントに返りません

RFC 6749 §3.1.2.4 に従い、検証できていない redirect_uri へは自動リダイレクトしません。代わりにテナントのエラー画面(ui_config.base_url + /error/)へ 302 します。

302 https://auth.example.com/error/?error=invalid_request
&error_description=redirect_uri does not match any registered redirect_uri, exact string matching failed (...)

クライアントの redirect_uri に invalid_request が返るのではなく、エンドユーザーがエラー画面で行き止まりになります。成功時も 302 なので、ステータスコードだけを見る監視では検知できません。

特に踏みやすいのは 11.2 のクエリ差です。大文字ホストや明示 :443 より、redirect_uri にクエリを付けて送っている実装のほうが在り得ます。現状は通っているため、確認せずに有効化すると気づかないまま停止します。

11.5 移行の想定​

  1. v0.13.0: フラグを既定 OFF で追加(このリリース)
  2. 検証環境で true にし、既存クライアントの redirect_uri が完全一致するか確認
  3. 次のメジャー: 既定を true に反転。フラグは移行期の緩和スイッチとして残す

11.6 動作確認​

# 完全一致(フラグの有無にかかわらず通る)
curl -i "$IDP/$TENANT/v1/authorizations?response_type=code&client_id=$CLIENT&redirect_uri=https://app.example.com/cb&scope=profile&state=s1"

# クエリ付き(フラグ OFF ならサインイン画面 / ON ならエラー画面)
curl -i "$IDP/$TENANT/v1/authorizations?response_type=code&client_id=$CLIENT&redirect_uri=https://app.example.com/cb%3Ffoo%3Dbar&scope=profile&state=s1"

どちらも 302 が返るため、Location の行き先で判定してください(サインイン画面か /error/ か)。

scope に openid を含めると OIDC プロファイルになり、フラグに関係なく完全一致で照合される点にも注意してください。

11.7 関連​


12. JWT アクセストークンに aud と sub が入るようになった(Issue #1824 / PR #1827)​

RFC 9068 が必須とするクレームのうち aud が発行されておらず、sub もリソースオーナーが介在しないグラントで欠落していました。条項と実装を対比して不足を埋めています。

12.1 aud が全ての JWT アクセストークンに入る(🔴 破壊的)​

RFC 9068 §2.2 は aud を REQUIRED としており、§3 は resource パラメータを伴わない要求でも既定リソース識別子を入れることを MUST としています。省略は許されません。

値はテナント設定から決まります。

設定aud
scope_resource_mapping でスコープが1つのリソースに対応そのリソース識別子
どのリソースにも対応しないdefault_resource_indicator
default_resource_indicator も未設定issuer
aud があるときだけ検証するリソースサーバーは落ちます

リソースサーバー側が「aud があれば検証する、無ければスキップする」実装だと、いままで通っていたトークンが落ちます。

 対応前: aud なし → 検証をスキップ → 通る
対応後: aud = issuer → 期待値と照合 → 落ちる

aud を指定して常に検証する実装(Spring Security の audience validator、jsonwebtoken の audience オプション等)は、対応前から不在で落ちているため影響ありません。影響を受けるのは自前で条件付き検証をしている実装です。

リソースを設定していないテナントほど踏みやすくなります(aud に issuer が入り、リソースサーバーが期待する自分の識別子とは一致しないため)。アップグレード前に、JWT アクセストークンを自前検証しているリソースサーバーの aud の扱いを確認してください。

aud はリソースサーバーの識別子であってクライアントではありません(クライアントは client_id クレームが表します)。ID トークンの aud がクライアントを指すのとは役割が逆です。設定方法は リソース単位のアクセストークン。

12.2 client_credentials のトークンに sub が入る(🟡 挙動変更)​

RFC 9068 §2.2 は、リソースオーナーが介在しないグラントでは sub がクライアントを表すべきとしています。従来は sub キー自体が返っていませんでした。

経路対応前対応後
JWT アクセストークンsub なしsub = client_id
introspectionsub なしsub = client_id

sub の存在をユーザーの存在と同一視している実装は影響を受けます。 監査ログで「ユーザー X が操作」と記録している場合、X にクライアント識別子が入ります。ユーザーの有無は username(リソースオーナー不在では引き続き返りません)か、グラント種別で判定してください。

introspection も併せて変えたのは、同じトークンを JWT として読むか introspect するかで結果が変わっていたためです。

12.3 複数のリソースにまたがるスコープ要求が拒否される(🟢 追加)​

scope_resource_mapping を設定したテナントでのみ有効です。未設定のテナントでは検証自体が走りません。

1つのアクセストークンは1つのリソースしか名乗れず、複数を名乗ると各リソースが他所向けのスコープを受け入れてしまうため、RFC 9068 §3 は要求の拒否を SHOULD としています。拒否はスコープが決まる場所で行われます。

スコープが決まる場所返り方
認可リクエストリダイレクトでの invalid_scope
CIBA バックチャネル認証リクエストレスポンスボディの invalid_scope(リダイレクト先が無いため)
トークンリクエスト(client_credentials / password / jwt-bearer)レスポンスボディの invalid_scope

リフレッシュでは検証しません。付与済みグラントのスコープは変えられないため、設定変更を理由に既発行トークンのリフレッシュを失敗させないためです。

設定は管理APIで追加・撤去でき、撤去すれば導入前の挙動に戻ります。認可サーバー設定の更新は全置換なので、GET した設定に項目を足して全体を PUT してください。

12.4 AccessToken#hasSubject() を削除(🟡 挙動変更)​

tokenSubject() に置き換わり参照が無くなったため削除しました。idp-server をライブラリとして組み込み、このメソッドを呼んでいる場合はコンパイルエラーになります。

12.5 未対応​

resource パラメータ(RFC 8707)による aud の指定は未対応です。永続化のための DDL を伴うため #1826 に分離しました。introspection が aud を返すのも同 Issue です。

12.6 動作確認​

ケーステスト
5グラント(認可コード / client_credentials / password / jwt-bearer / CIBA)の §2.1・§2.2 準拠e2e/.../spec/rfc9068_jwt_access_token.test.js
各経路での invalid_scope 拒否同上
リフレッシュが元のグラントの aud を保つ同上
設定の導入・差し戻しで挙動が戻ることe2e/.../usecase/standard/standard-31-resource-indicator-rollout.test.js
introspection の sub が JWT と一致することe2e/.../spec/rfc7662_token_introspection.test.js

13. RFC 6749 準拠強化 — client_credentials の confidential 限定と Basic 認証のデコード(Issue #1820 / PR #1855)​

RFC 6749 の条項と実装を対比し、不足していた準拠事項を実装しました。うち 2 件は既存クライアントの挙動を変えます。

アップグレード前チェックスクリプトで一括確認できます

本セクションの 3 つの棚卸しを、設定ファイルとデータベースの両方に対してまとめて実行できます。

# 設定ファイルのみ(既定は ./config を走査)
./config/scripts/upgrade-check-v0.13.0.sh

# 自分の設定パスを指定。-d と -x は繰り返し指定できる
./config/scripts/upgrade-check-v0.13.0.sh -d /etc/idp/clients -x '/examples/'

# データベースも検査する
./config/scripts/upgrade-check-v0.13.0.sh -p "postgresql://user:pass@host:5432/idpserver"
./config/scripts/upgrade-check-v0.13.0.sh -m "-h host -u user -ppass idpserver"

終了コードは 0 = 該当なし / 1 = 該当あり / 2 = 実行エラー なので、デプロイパイプラインのゲートにも置けます。

設定ファイルとデータベースの両方を確認してください。 管理 API で動的に登録したクライアントは 設定ファイルには現れず、逆にまだ投入していない設定はデータベースに現れません。

各項目の個別 SQL は以下の各セクションにも記載しています。スクリプトを使わず手で確認する場合はそちらを参照してください。

13.1 public client が client_credentials を使えなくなった(🔴 破壊的)​

The client credentials grant type MUST only be used by confidential clients. — RFC 6749 §4.4

token_endpoint_auth_method: none のクライアントは資格情報を提示しないため、クライアント認証を素通りしていました。grant_types に client_credentials を持つ public client が、無認証でアクセストークンを取得できる状態でした。

クライアント対応前対応後
none かつ grant_types に client_credentials あり200 でトークン発行401 invalid_client
none かつ client_credentials なし400 unauthorized_client変わらず
confidential(client_secret_* / private_key_jwt / mTLS)200変わらず
該当するクライアントはアップグレードで即座に 401 になります

設定として成立してしまっていたため、意図せず該当している可能性があります。以下で棚卸ししてください。

select payload->>'client_id', payload->>'client_id_alias'
from client_configuration
where payload->>'token_endpoint_auth_method' = 'none'
and payload->>'grant_types' like '%client_credentials%';

該当した場合、設定を confidential に変えるのが正しい対処です。client_credentials は「クライアント自身がリソースの持ち主として動く」グラントであり、資格情報を秘匿できないクライアントが使うことは仕様上認められていません。フラグによる緩和は用意していません。

なお none の拒否は経路ごとに独立しています。CIBA・introspection・device endpoint は以前から拒否しており、authorization_code / refresh_token / password / revocation / PAR は仕様上 public client を許容するため拒否しません(過剰に締めていないことを E2E で担保しています)。FAPI 拡張にも同種の判定がありますが、そちらはプロファイル適用時のみ効きます。

13.2 Basic 認証のクレデンシャルをデコードするようになった(🔴 破壊的)​

The client identifier is encoded using the "application/x-www-form-urlencoded" encoding algorithm per Appendix B ... the client password is encoded using the same algorithm — RFC 6749 §2.3.1

このエンコードは体裁ではなく パースを一意にするための前提です。コロンが %3A になることでリテラルのコロンがちょうど 1 個になり、user-id:password の分割が常に正しくなります。

デコードしていなかったため、次の 2 つが起きていました。

  • 仕様どおり percent-encode して送るクライアントが認証に失敗する
  • client_id_alias にコロンを含むクライアントが、別の(切り詰められた)client_id に解決される

client_id_alias はスキーマ上コロンを登録できます({"type":"string","maxLength":"255"})。

平文の client_secret送信のしかた対応前対応後
abc123(記号なし)そのまま通る変わらず通る
a%3Ab仕様どおり encode❌ 落ちる✅ 通る
a+b生のまま✅ 通る⚠️ a b と解釈され落ちる
a%b生のまま✅ 通る⚠️ 落ちる
平文シークレットに % + を含む Basic クライアントは影響を受けます

Appendix B は HTML フォームエンコードなので、+ はスペースを表し、リテラルの + は %2B として送られます。生の + を送っているクライアントは、対応後はスペースとして照合されて落ちます。

select count(*) from client_configuration
where payload->>'token_endpoint_auth_method' = 'client_secret_basic'
and payload->>'client_secret' ~ '[%+]';

0 件であれば影響はありません。該当がある場合は、シークレットを記号なしに更新するか、クライアント側を仕様どおりの encode に直してください。

この非互換は緩和できません。デコード版と生版の両方を受理すると 1 つのシークレットに 2 つの正解を作ることになり、認証の照合を緩めることになるためです。

影響範囲は client_secret_basic の経路のみです。client_secret_post を含む他の認証方式は通りません。管理API の Basic 認証も通りません(RFC 6749 §2.3.1 はトークンエンドポイントのクライアント認証の規定であるため、メソッドを分離しています)。

13.3 Basic の Base64 が標準アルファベットのみになった(🔴 破壊的)​

RFC 7617 の Basic は標準 Base64(RFC 4648 §4)ですが、URL-safe デコーダを使っていました。URL-safe は + と / を受け付けないため、符号化結果にそれらを含む準拠クライアントが無診断で認証失敗していました。Base64 文字の 2/64 が + / なので、base64 生成のシークレットはほぼ確実に踏みます。

送信側の符号化対応前対応後
標準 Base64(RFC 7617 準拠)+ / が出ると 落ちる✅ 通る
URL-safe Base64(非準拠)通る⚠️ 落ちる

壊れる集団が入れ替わります。 準拠クライアントが救われる一方で、URL-safe で組んでいる非準拠クライアントは認証できなくなります。

URL-safe Base64 で Basic ヘッダを組んでいるクライアントは落ちます

影響を受けるのは、符号化結果が 2 つのアルファベットで実際に食い違う場合だけです。標準と URL-safe はインデックス 62・63 の 2 文字しか違わないため、+ /(URL-safe 側では - _)が出なければ両者は完全に同じ文字列になり、どちらで組んでも通ります。

ASCII の識別子・シークレットで + / が出るのは、> ? ~ が 3n+2 バイト目に来たときだけです。英数字・16 進のみのシークレットでは構造的に出ません。

select count(*)
from client_configuration
where payload->>'token_endpoint_auth_method' = 'client_secret_basic'
and (
translate(encode(convert_to(coalesce(payload->>'client_id_alias', payload->>'client_id') || ':' || coalesce(payload->>'client_secret',''), 'UTF8'), 'base64'), chr(10), '') ~ '[+/]'
or
translate(encode(convert_to((payload->>'client_id') || ':' || coalesce(payload->>'client_secret',''), 'UTF8'), 'base64'), chr(10), '') ~ '[+/]'
);

0 件であれば、クライアント側の実装にかかわらず影響はありません。該当がある場合は、そのクライアントが Basic ヘッダを標準 Base64 で組んでいるかを確認してください。

一時的に両方のアルファベットを受け入れる移行措置も検討しましたが、採用していません。受理する集合を広げる措置であるうえ、その存在意義(非準拠クライアントの炙り出し)を果たすためのログが、クライアント認証より前・公開エンドポイントで動くため、任意のヘッダを投げるだけで無制限に出力させられるためです。

診断だけは残しています。標準での復号に失敗したとき、URL-safe なら解けるかを判定のためだけに試し、結果を DEBUG で記録します(資格情報そのものは出力しません)。原因不明の invalid_client を調査する際に、DEBUG を有効化すれば「クライアントが base64url で組んでいる」ことが分かります。

platform 側の BasicAuthConvertable は #1245 で同じ誤りを修正済みで、core 側が追随できていませんでした。

13.4 動作確認​

ケーステスト
client_credentials が confidential client 限定であることe2e/.../spec/rfc6749_4_4_client_credentials.test.js
public client が許容されるグラント(ROPC)は通ることe2e/.../spec/rfc6749_4_3_resource_owner_password_credentials.test.js
Appendix B デコード / 標準 Base64 / URL-safe フォールバックlibs/idp-server-core/.../AuthorizationHeaderHandlerableTest.java
§2.3.1 のデコードが実サーバーで効くことe2e/.../spec/rfc6749_4_1_code.test.js
別クライアントによる認可コード交換の拒否(§10.2)同上
refresh token のクライアント束縛(§10.4)e2e/.../spec/rfc6749_6_refresh_token.test.js

13.5 あわせて入った準拠テスト(挙動変更なし)​

対比の結果、実装は既に準拠していたものの E2E が無かった条項にテストを追加しています。挙動は変わりません。§3.2(POST 限定・パラメータ重複禁止)、§3.3(scope 省略時・発行スコープの応答)、§5.1(Cache-Control: no-store / Pragma: no-cache)、§10.13(X-Frame-Options)。

§2.3(複数のクライアント認証方式の同時提示)は、MUST の主語がクライアントでサーバー側の拒否義務が無いため未対応のままとし、台帳として xit で残しています。


14. 番号マッチングの検証に device_id が必須になった(Issue #1869 / PR #1870)​

番号マッチングのコードは、設計上認証デバイスには送られません。サインイン画面に表示された値を利用者が読み取り、デバイスへ転記する前提で成り立っています(プッシュ通知に対する blind approval を防ぐため)。

これまで検証は転記されたコードが保存値と一致するかだけを見ており、提出元のデバイスを確認していませんでした。転記の前提が守られているかをサーバ側で確認する手段が無く、承認したのが対象のデバイスであることが担保されていませんでした。

リクエストに device_id を要求し、認証トランザクションに紐づく認証デバイスと照合するようにしています。

BeforeAfter
リクエストボディ{ "number_matching_code": "4821" }{ "device_id": "...", "number_matching_code": "4821" }
照合コードのみdevice_id → コードの順
デバイス未紐付けのトランザクションコードが一致すれば成立成立しない(fail-closed)

14.1 デバイスアプリの改修(🔴 破壊的)​

device_id は認証トランザクション取得API のパスに用いるものと同じ値です。

GET /{tenant-id}/v1/authentication-devices/{device-id}/authentications

デバイスは自身のトランザクションを引くためにこの値を既に保持しているため、新しく取得するものはありません。リクエストボディに 1 項目追加するだけです。

{
"device_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number_matching_code": "4821"
}

device_id を送らない既存のデバイス実装は 400 invalid_request になります。アップグレード前にデバイスアプリを更新してください。

14.2 エラーの追加​

device_id の照合はコードの照合より先に行われます。対象外のデバイスからの提出に、コードの一致・不一致を返さないためです。

状況HTTPerror_description
device_id 未指定400Field 'device_id' is required
認証デバイス未紐付け400authentication device is not bound to this transaction
device_id 不一致400device_id does not match the authentication device
チャレンジ未実行400number_matching_code has not been issued(既存)
コード不一致400number_matching_code does not match(既存)

既存 2 種の error_description は変わりませんが、到達するには device_id が必要になりました。エラー文言で分岐している実装は、先に device_id を送るよう直してください。

14.3 デバイス未登録の利用者(🔴 破壊的)​

認証デバイスが紐づいていない認証トランザクションでは、番号マッチングが成立しなくなりました。素通しにすると照合が意味を持たなくなるためです。

認証デバイスへの承認を前提とした仕組みであり、デバイス未登録の利用者にこのステップを課す構成は元々成立していません。認証ポリシーで番号マッチングを必須にしているテナントは、対象利用者に認証デバイスが登録されているか確認してください。

トランザクションへの紐付けは、通常のログインでは 1 要素目の成功時に利用者の主デバイス(priority 最大)が解決されます。login_hint で利用者を指定した場合は認可リクエストの受付時に解決されます。

14.4 変わらないもの​

  • amr / acr の値。operationType は変更していません
  • success_conditions / failure_conditions の書き方。$.authentication-device-number-matching.success_count 等はそのまま使えます
  • チャレンジ側(authentication-device-number-matching-challenge)のリクエスト・レスポンス
  • 保存済みデータ。DB マイグレーションはありません

14.5 動作確認​

ケーステスト
デバイス照合(成功 / 別デバイス / 未指定 / 未紐付け / 誤コード)AuthenticationDeviceNumberMatchingInteractorTest(unit)
実環境でのデバイス紐付けe2e/.../security/number-matching-device-binding.test.js
デバイスが持てる id だけでの検証e2e/.../scenario/application/scenario-06-oauth-fido-uaf-login-hint.test.js

15. execution に渡る $.user 投影を利用者の確立源で判定するようになった(Issue #1862 / PR #1863)​

認証 interaction の execution.http_request は、マッピングソースとして $.user.*(許可リスト付きの利用者属性)を受け取れます。これまでは external-api だけがこれを渡しており、password / sms / email では同じ記法が使えませんでした。

本バージョンで 5 つの interaction に投影を追加し、あわせて投影してよいかの判定を「利用者の確立源」に統一しました。

15.1 external-api の投影条件が変わった(🟡 挙動変更)​

external-api はこれまで無条件に投影していました。判定が入ったことで、次の表のとおりになります。

利用者の確立源BeforeAfter根拠
このトランザクションで認証 interaction が成功している✅✅要素を 1 つ通っている
CIBA の login_hint✅✅backchannel エンドポイントはクライアント認証を通してからリクエストを受理する(CibaRequestHandler:121)。名指しした利用者をクライアントが保証している
認可エンドポイントの login_hint✅❌ 空になる認可エンドポイントにクライアント認証は無く(OAuthFlowEntryService:183)、誰でも任意の利用者を名指しできる
認可エンドポイントの login_hint に依存している構成は影響を受けます

external-api の execution で $.user.* をマッピングし、1 要素目を認可エンドポイントの login_hint で解決していた構成では、その値が空になります。マッピングは解決しないだけでエラーにはならないため、外部 API 側でパラメータ不足として現れます。

投影が必要であれば、先に認証 interaction を 1 つ通す構成にしてください。CIBA 経由の login_hint は従来どおり 1 回目から投影されます。

15.2 password / sms / email から $.user.* が使えるようになった(🟢 追加)​

対象は次の 5 interaction です。既存設定は $.user.* を参照していないため影響はありません。

  • password-authentication
  • sms-authentication-challenge / sms-authentication
  • email-authentication-challenge / email-authentication

投影される属性の許可リストは external-api と共通です(詳細は External API認証の設定リファレンス)。hashed_password / verified_claims などの秘匿値は含まれません。

15.3 変わらないもの​

  • 投影される属性の許可リスト。ExternalRequestUserContextCreator は変更していません
  • egress は opt-in のまま。テナント管理者がマッピングルールを書いたフィールドだけが外部に出ます
  • user_resolve 側の投影(#1767)
  • DB マイグレーションはありません。判定は永続化済みの flow と interactions から導出します

15.4 動作確認​

ケーステスト
確立源の境界(利用者なし / login_hint / 認証成功 / 認証失敗 / CHALLENGE のみ成功 / CIBA 1 回目)AuthenticationTransactionTrustedUserTest(unit)
password の投影とリクエストボディによる偽装不可PasswordAuthenticationInteractorExecutionRequestTest(unit)
実環境での投影・許可リスト・login_hint の非投影e2e/.../security/password-authentication-user-context.test.js
CIBA 1 回目からの投影e2e/.../security/ciba-password-user-context.test.js

16. クライアントの custom_properties が認証デバイスからも読めるようになった(Issue #1833 / PR #1834)​

ClientAttributes#toMap() に custom_properties を追加しました。サインイン画面は認可ビューデータの client_custom_properties で既に受け取っていましたが、認証デバイスはクライアントを toMap() 経由でしか見ないため、これまで参照できませんでした。

認証デバイスに渡って良い内容か確認してください(🔒 要点検)

custom_properties はテナントが自由に定義できる領域です。追加により次の経路に現れます。

出力先経路
認証トランザクション(認証デバイス向け API のレスポンス)AuthenticationRequest#toMap() の client_attributes
authentication_transaction.client_payload(永続)同上
oauth_token.client_payload(永続)AuthorizationGrant#clientAttributes()

クライアントの custom_properties に内部管理用の識別子や運用メモを入れている場合、認証デバイスから読めるようになります。設定内容を確認してください。

出力は hasCustomProperties() が真のときだけです。custom_properties を設定していないクライアントではキー自体が出ません。

16.1 動作確認​

ケーステスト
custom_properties の保持・null 畳み込み・toMap() 出力ClientAttributesTest(unit)
認証デバイスからの参照e2e/.../scenario/application/scenario-06-oauth-fido-uaf-login-hint.test.js

付録: 登録不要と判定した変更​

🟢 追加 / ⚙️ 内部・運用のみで、既存の設定・クライアントの挙動を変えないと判定したもの。「未対応」と「そもそも該当が無い」を区別するために残す。

PR変更種別判定理由
#1735身元確認設定JSONから OpenAPI 仕様書を自動生成するスクリプト⚙️ドキュメント生成のみ。ランタイム不変
#1368パスワードレスFIDO2ユースケースの実験ガイド⚙️ドキュメント・スクリプトのみ
#1737FAPI 2.0 / DPoP follow-up クラスタA⚙️対象は fapi2-tenant のサンプル設定とドキュメント。既存テナント不変
#1751HttpRequestInputs 導入 + 全エンドポイント DTO を canonical 形へ⚙️DTO の公開アクセサ名を維持しており Handler / Verifier 層は無変更。リクエスト・レスポンスの形は不変(E2E フルスイート 2,052 件で確認)
#1766number-matching のプロトコル定義・OpenAPI・設定リファレンス⚙️既存実装のドキュメント化
#1769認可コードフローの認証を CIBA へ委譲する構成🟢新規テンプレート・E2E の追加。※出荷テンプレートの認証バイパスは 2.3 に登録済み
#1775mapping の JSONPath 述語の文書化と Jayway 挙動のテストピン留め⚙️ドキュメント + テストのみ
#1778使用中の Nimbus JOSE 挙動とラッパー契約のピン留め⚙️テストのみ
#1782mapping function に base64 を追加🟢新規関数。既存設定は参照しない
#1798http_requests を条件付きで実行できるようにする🟢condition は未設定なら無条件実行。既存設定は不変。単数 http_request では inert
#1805mapping function に normalize を追加🟢新規関数。既存設定は参照しない
#1812認可コードフロー + FIDO-UAF の全体像図と想定ケース⚙️ドキュメント + E2E
#1814相対リンクの階層ずれ 259 件を修正⚙️ドキュメントのみ
#1815step_definitions を interaction 単位で解決🟢既存の定義はいずれも interaction を持たないため method 単位の解決にフォールバックし、挙動は変わらない。toMap も interaction を持つときだけキーを出すため管理APIの往復も不変
#1818v0.13.0 影響確認に 39 件のトリアージ結果を登録⚙️本ドキュメント自体の作成
#1829学習コンテンツに RFC 10017(Browser-Based Applications)を追加⚙️ドキュメントのみ
#1831認可画面をリクエスト単位で出し分けられるようにする(カナリアリリース)🟢variants 未設定なら resolveVariant が空の variant を返し、tenant.baseUrl() + 既定パスにフォールバックする(OAuthViewUrlResolver)。既存テナントの URL は不変。variant 名は宣言へのキーとしてのみ使われ、パスには連結されない(認可URLは公開されるため)
#1832FIDO-UAF 認可コードフローのデバイス側実装の記載補完 + number-matching / deny を OpenAPI に追加⚙️ドキュメント + 仕様書のみ。ランタイム不変
#1839残りの zsh shebang スクリプト 5 本を bash に統一⚙️開発用スクリプトのみ
#1843認証トランザクションの ?id= 絞り込みが 500 になるのを修正🟢管理APIのクエリフィルタの不具合修正。従来 500 だった経路が正常に応答する
#1844OIDF 適合性テスト 4 スイートのローカル自動実行⚙️テスト実行基盤のみ。ランタイム不変
#1850Spring Boot 4.0.7 → 4.0.8⚙️依存のパッチ更新。BOM 経由で依存スキャンの検出 22 件中 18 件を解消
#1853FAPI 2.0 gap analysis を適合性テストの実測に合わせて更新⚙️ドキュメントのみ
#1859oidcc テナントに acr_mapping_rules を追加⚙️対象は config/examples/oidcc-cross-site* の適合性テスト用設定。既存テナント不変
#1872v0.13.0 影響確認のトリアージを 56 件時点まで更新⚙️本ドキュメント自体の更新
#1873OIDF 適合性ドライバの passkey / email 食い違いを起動時に検出⚙️対象は oidc-conformance-suite/driver/。テスト実行基盤のみでランタイム不変

付録の注記​

  • external-api のステップは汎用サインイン画面(app-view)で描画できません(#1815)。interaction ごとの入力項目はテナント設定の request.schema で定義されるため、何を入力させるかは実行時までわからず、固定のコンポーネントでは覆えません。実装漏れではなく方式の性質です。
  • step_definitions の order / user_identity_source / verification_source / registration_mode は未実装のままです(参照 0 箇所)。とくに order は現在無視されているため、実装すると順序が誤っている既存設定が失敗するようになります。将来のリリースで挙動変更になる候補です。