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

v0.15.0 影響確認

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

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

影響まとめ​

#変更種別対象
1外部IdPアカウント連携を追加(新モジュール idp-server-account-linking)。DBマイグレーション 2 本(V0_15_0_1、V0_15_0_2)が必要🟢 追加 / ⚙️全環境(マイグレーション必須)
1エンドユーザー向けの連携 API(/{tenant-id}/v1/me/linked-external-accounts)、保管トークンの取得 API(POST /{tenant-id}/v1/linked-external-accounts/{alias}/token)、ブラウザ遷移用の /{tenant-id}/v1/linking/start / /linking/callback/{provider} を追加🟢 追加アカウント連携を使う構成。設定しない限り挙動は変わらない
1スコープ linked_account:read / linked_account:manage / linked_account:read_token を新設。テナントの scopes_supported とクライアントの scope の両方に追加しないと常に 403🟢 追加 / 🔒 要点検アカウント連携を使う構成
1クライアントの extension に linking_return_uris(連携後の戻り先)、linking_providers(連携を始められる連携先)、linking_token_providers(保管トークンを取り出せる連携先)、linking_max_auth_age(連携に使えるログインの新しさ、既定 300 秒)、linking_acr_values(連携に求めるログインの acr)を追加。未設定なら連携の開始は 400 / 403、保管トークンの取得は 403🟢 追加 / 🔒 要点検アカウント連携を使うクライアント
1すべてのテナントのログインで __Host-IDP_BROWSER_{tenant-id} Cookie(Secure・HttpOnly・Path=/)を 1 つ多く発行する。/linking/start だけがこれを見て、このブラウザがログインして受け取ったセッションかを確かめる。v0.15.0 より前の OP セッションで連携を始めると 1 回ログインし直し🟡 挙動変更全環境(Set-Cookie が増える)。連携は HTTPS 必須・Cookie のパスを書き換えるプロキシでは不可
1/linking/start はブラウザのログインが無いか古いとき、連携を始めた本人としてその場でログインし直させる(認可コードは発行しない)。認証画面(app-view)はこのログインでは同意を省く🟢 追加アカウント連携を使う構成
1連携先は federation-configurations に新しい type: account-linking で登録する。payload に revocation_endpoint / duplicate_link_policy(既定 reject)/ multiple_accounts(既定 false)を追加🟢 追加アカウント連携の連携先を登録する構成
1連携先(type: account-linking)の federation-configurations は、登録・更新のときに中身を検査し(必須のエンドポイント、この連携先のコールバック、duplicate_link_policy の値、openid なら issuer / jwks_uri)、使えなければ 400。type と sso_provider は変更できない(400)。連携が残っている間の削除は 409(dry_run=true でも同じ)🟡 挙動変更アカウント連携の連携先を管理する自動化
1管理権限 idp:linked-account:read / idp:linked-account:delete と、ユーザーの連携を確認・解除する管理API(システム・組織)を新設。既存テナントには権限のレコードが無い🟢 追加 / 🔒 要点検権限を明示列挙しているロール(idp:* を持つロールは自動的に含む)
1管理権限 idp:data-key:read / idp:data-key:rotate と、テナントのデータ鍵を一覧・新しい世代を作る管理API(システム・組織)を新設。既存テナントには権限のレコードが無い🟢 追加 / 🔒 要点検権限を明示列挙しているロール(idp:* を持つロールは自動的に含む)
1セキュリティイベント型が 8 種増えた(external_account_*)🟢 追加セキュリティイベントフック / SIEM 連携
1POST /v1/admin/operations/delete-expired-data のレスポンス内訳に account_linking_session が増えた🟡 挙動変更当該レスポンスを厳密に検証している自動化
1保管トークンを 2 段の鍵(テナントごとのデータ鍵を鍵暗号化鍵で包む)で暗号化する。環境変数 ENCRYPTION_KEYS / ENCRYPTION_ACTIVE_KEY_ID(任意)と、運用API /v1/admin/operations/data-keys・/data-keys/rewrap・/data-keys/rotate を追加🟢 追加アカウント連携を使う構成。未設定なら ENCRYPTION_KEY で動く
1運用API(/v1/admin/operations/*)は本文を省いたリクエストを空の本文として扱う。これまで delete-expired-data で本文を省くと 500 だった🟡 挙動変更なし(500 が返らなくなるだけ)
1delete-expired-data の max_deletion_number は 1〜100000 の整数に限る。範囲外や数でない値は、これまでの 500 ではなく 400 を返す🟡 挙動変更範囲外の値を渡している自動化
1連携は既定で 1 ユーザー×連携先に 1 件。もう一度連携すると更新する(別の外部アカウントは 409)。multiple_accounts: true の連携先では複数持てる。識別子(alias)はどちらも {provider}-{8 文字の乱数} で、解除した連携の識別子は別の連携に使い回さない🟢 追加識別子の形を前提にしている RP
1ユーザーの削除で、そのユーザーの連携と連携セッションも削除し、保管トークンを外部IdPで失効させる(ベストエフォート)🟢 追加なし(連携が無いユーザーは変わらない)
1テナントの削除で、削除の前にテナントの全ての連携の保管トークンを外部IdPで失効させる(ベストエフォート)。連携 1 件ごとに外部IdPを呼ぶので、連携が多いテナントは削除に時間がかかる🟡 挙動変更連携があるテナントを削除する運用
1付属の認可画面(app-view)が、入力した識別子とユーザーの状態を認可リクエストごとに保存する。同じタブの次のフローに、前のフローで入力したメールアドレスが入らなくなった🟡 挙動変更app-view をそのまま使う構成
1付属の認可画面(app-view)の同意画面に、新しいスコープの表示名を追加🟢 追加app-view をそのまま使う構成
1FederationConfigurationQueryRepository に get(..., boolean includeDisabled) を追加(default 実装なし)⚙️このインターフェースを自前で実装している構成
1設定例 config/examples/standard-oidc-web-app/ の public-client.json を client_secret_basic にし、連携のスコープと extension を追加。連携用の設定例を追加⚙️この設定例をそのまま使っているローカル環境

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


1. 外部IdPアカウント連携(Issue #1531)​

🟢 追加。 認証済みのユーザーが、自分のアカウントに外部IdPのアカウントを追加で紐付ける機能を追加した。紐付けのときに外部IdPが発行したアクセストークンとリフレッシュトークンを暗号化して保管し、クライアントが後から取り出して外部APIの呼び出しに使える。

ログイン用の Federation とは別の系統で、連携を追加しても Federation の動作は変わらない。設定しないテナント・クライアントの動きも変わらない。

詳細は 外部IdPアカウント連携。

1.1 DBマイグレーション​

⚙️ 全環境で必須。

ファイル内容
libs/idp-server-database/postgresql/V0_15_0_1__account_linking.sqllinked_external_accounts / account_linking_sessions テーブル + index 4 本 + RLS
libs/idp-server-database/mysql/V0_15_0_1__account_linking.mysql.sql同(MySQL に RLS は無いため tenant_id 述語のみ)
libs/idp-server-database/postgresql/V0_15_0_2__tenant_data_keys.sqltenant_data_keys テーブル + RLS、linked_external_accounts.encryption_key_id の index と既定値の削除
libs/idp-server-database/mysql/V0_15_0_2__tenant_data_keys.mysql.sql同(「active なデータ鍵は 1 本」を生成列の UNIQUE で表す)

V0_15_0_1 で作ったテーブル以外への変更は無い。tenant_id はテナントの削除に連動して消える(ON DELETE CASCADE)。user_id には外部キーを張っていないため、ユーザー削除時の後始末はアプリケーションが行う(1.8)。

トークンはテナントごとのデータ鍵で暗号化し、データ鍵は鍵暗号化鍵で包んで tenant_data_keys に置く。鍵暗号化鍵は ENCRYPTION_KEYS で渡し、無ければ ENCRYPTION_KEY を使うため、追加の設定なしで動く。鍵の交換は データ鍵と鍵暗号化鍵のローテーション を参照。

oauth_token の暗号化はこれまでどおり ENCRYPTION_KEY を直接使う。ENCRYPTION_KEYS を設定しても ENCRYPTION_KEY は必須のままで、値を替えてはいけない。

1.2 追加したエンドポイント​

🟢 追加。

POST   /{tenant-id}/v1/me/linked-external-accounts/link/{provider}   スコープ linked_account:manage
POST /{tenant-id}/v1/me/linked-external-accounts/complete スコープ linked_account:manage
GET /{tenant-id}/v1/me/linked-external-accounts スコープ linked_account:read または linked_account:manage
DELETE /{tenant-id}/v1/me/linked-external-accounts/{alias} スコープ linked_account:manage

POST /{tenant-id}/v1/linked-external-accounts/{alias}/token クライアント認証 + token に linked_account:read_token

GET /{tenant-id}/v1/linking/start?state= ブラウザセッション
GET /{tenant-id}/v1/linking/callback/{provider}?code&state state + ブラウザ束縛

/linking/start で __Host-IDP_LINK_BINDING_{tenant-id} Cookie(HttpOnly、Secure、Path=/、Domain なし)を発行し、コールバックで照合する。この Cookie の SameSite は Lax 固定で、Secure・Path・Domain も含めてテナントの session_config には従わない。そのため次の構成では連携が成立しない。

  • start_url を iframe の中で開く構成(トップレベル遷移かポップアップで開く)
  • 外部IdPが response_mode=form_post で返す構成(コールバックは GET のみ)
  • idp-server を平文の HTTP で公開している構成(__Host- の Cookie は HTTPS でしか保存されない)
  • リバースプロキシや API Gateway が Cookie のパスを書き換える構成(proxy_cookie_path など。__Host- の Cookie は Path=/ 以外だと保存されない)
  • cookie_domain で複数のホストにまたがり、ログインを終えるホストと /linking/start のホストが違う構成(下のブラウザの証が届かず、毎回ログインし直しになる)

あわせて、すべてのテナントのログインで __Host-IDP_BROWSER_{tenant-id} Cookie を 1 つ多く発行する(乱数。OP セッションにはそのハッシュだけを持つ)。/linking/start は、このブラウザがログインして受け取ったセッションかをこの値で確かめる。見るのは /linking/start だけで、SSO などほかの挙動は変わらない。v0.15.0 より前に作られた OP セッションはこの値を持たないので、連携を始めると 1 回ログインし直しになる。

1.3 スコープ​

🔒 要点検。

スコープ許可すること
linked_account:read連携の一覧
linked_account:manage連携の開始・確定・解除と一覧
linked_account:read_token保管トークンの取得。read / manage のどちらにも含まれず、どちらも含まない
"scopes_supported": ["openid", "profile", "email", "linked_account:read", "linked_account:manage", "linked_account:read_token"]

テナントの scopes_supported とクライアントの scope の両方に追加する必要がある。片方だけではトークンにスコープが載らず、/me の連携 API は常に 403 insufficient_scope になる。保管トークンの取得も token に linked_account:read_token が無ければ 403 insufficient_scope。

1.4 クライアント設定​

🔒 要点検。 クライアントの extension に 3 つ追加した。どれも既定は空。

{
"extension": {
"linking_return_uris": ["https://rp.example.com/linking/callback"],
"linking_providers": ["example-idp"],
"linking_token_providers": ["example-idp"]
}
}
項目内容未設定のとき
linking_return_uris連携が終わったあとにブラウザを戻す先。link の redirect_uri と完全一致で照合する。認可の redirect_uris とは別link が 400
linking_providers連携を始められる連携先(sso_provider)link が 403
linking_max_auth_age/linking/start がそのまま受け入れるログインの新しさ(秒)。古いかログインが無ければ、その場で本人としてログインし直させる。0 で毎回300
linking_acr_values連携に求めるログインの acr(好ましい順)。ログインが新しくても acr がどれにも当たらなければログインし直させ、そのログインの認可リクエストに acr_values として載せる(acr_values で選ばれる認証ポリシーが当たる)。戻ってもまだ当たらなければ login_required制限なし(新しさだけで判断)
linking_token_providers保管トークンを取り出せる連携先(sso_provider)保管トークンの取得が 403 unauthorized_client

link の scope は連携先の設定の scopes_supported の範囲に限り、範囲外は 400。連携先の scopes_supported に openid があるときは、scope に openid を含める必要がある(無ければ 400)。外部アカウントを ID Token の sub で識別するため。

保管トークンの取得は、クライアント認証に成功したクライアントだけが使える。public クライアント(none)は 401 invalid_client。

1.5 連携先の設定​

🟢 追加。 連携先は既存の federation-configurations に、新しい type: account-linking で登録する。payload はログイン用の OIDC の設定(type: oidc)と同じ形。ログイン用の設定とは互いに使われず、同じ sso_provider の名前を付けられる。redirect_uri には /{tenant-id}/v1/linking/callback/{sso_provider} を指定する。payload で、連携だけが読むキーを 3 つ追加した。

キー内容省略時
revocation_endpoint解除とユーザー削除のときに保管トークンを失効させる先(RFC 7009)外部IdP側のトークンは失効させず、連携だけを削除
duplicate_link_policy同じ外部アカウントが既にテナント内の別ユーザーに紐付いているときの挙動。reject(409)/ allowreject
multiple_accounts1 人がこの連携先のアカウントを複数連携できるか。false なら 1 件で、もう一度連携すると更新、別の外部アカウントは 409false

保管するリフレッシュトークンの期限には、既存の refresh_token_expires_in を使う。未設定なら期限は不明として扱い、リフレッシュを試す。

連携は type と sso_provider の組で連携先を引くため、連携先の type と sso_provider は変更できない(PUT が 400)。連携が残っている連携先の DELETE .../federation-configurations/{config-id} は 409 で拒否する。消すと、残った連携の保管トークンを取り出せず、失効もさせられなくなるため。応答の linked_external_accounts が残っている連携の数で、先に 管理API で解除する。dry_run=true でも同じ判定をする。

登録と更新のときは中身も確かめ、使えなければ 400 を返す(error_messages に理由)。必須は authorization_endpoint / token_endpoint / client_id / redirect_uri。redirect_uri はこの連携先のコールバック(/{tenant-id}/v1/linking/callback/{sso_provider})、duplicate_link_policy は reject か allow、multiple_accounts は真偽値、scopes_supported に openid があれば issuer と jwks_uri も要る。

1.6 管理権限と管理API​

🔒 要点検。 連携を確認・解除する管理API と、その権限を新設した。ユーザー単位のものと、テナント全体のもの(連携先・ユーザー・連携日時で絞り込み、total_count を返す)がある。保管しているトークンはどれも返さない。

GET    /v1/management/tenants/{tenant-id}/users/{user-id}/linked-external-accounts                                  idp:linked-account:read
GET /v1/management/tenants/{tenant-id}/users/{user-id}/linked-external-accounts/{alias} idp:linked-account:read
DELETE /v1/management/tenants/{tenant-id}/users/{user-id}/linked-external-accounts/{alias} idp:linked-account:delete
GET /v1/management/organizations/{org-id}/tenants/{tenant-id}/users/{user-id}/linked-external-accounts idp:linked-account:read
GET /v1/management/organizations/{org-id}/tenants/{tenant-id}/users/{user-id}/linked-external-accounts/{alias} idp:linked-account:read
DELETE /v1/management/organizations/{org-id}/tenants/{tenant-id}/users/{user-id}/linked-external-accounts/{alias} idp:linked-account:delete
GET /v1/management/tenants/{tenant-id}/linked-external-accounts idp:linked-account:read
GET /v1/management/tenants/{tenant-id}/linked-external-accounts/{id} idp:linked-account:read
DELETE /v1/management/tenants/{tenant-id}/linked-external-accounts/{id} idp:linked-account:delete
GET /v1/management/organizations/{org-id}/tenants/{tenant-id}/linked-external-accounts idp:linked-account:read
GET /v1/management/organizations/{org-id}/tenants/{tenant-id}/linked-external-accounts/{id} idp:linked-account:read
DELETE /v1/management/organizations/{org-id}/tenants/{tenant-id}/linked-external-accounts/{id} idp:linked-account:delete

DELETE は dry_run=true を付けると、解除せずに結果だけを返す。

idp:* を持つロール(既定の administrator)は自動的に含む。 権限を明示列挙しているロールで使うには、次の手順が要る。

  1. テナントに権限のレコードを作る。権限のレコードはテナントのオンボーディングのときに作られるため、既存のテナントには idp:linked-account:read / idp:linked-account:delete が無い。POST .../tenants/{tenant-id}/permissions で作成する
  2. 作成した権限をロールに追加する。ロールの登録・更新はテナントに存在する権限しか受け付けない

1.7 セキュリティイベント型の追加​

🟢 追加。 8 種増えた。

イベント型記録すること
external_account_linked連携の確定
external_account_unlinked本人による解除
external_account_unlinked_by_admin管理APIによる解除
external_account_link_failed連携の失敗
external_account_token_retrieved保管トークンの払い出し
external_account_token_retrieval_failed保管トークンの払い出しの拒否
external_account_token_refreshed保管トークンの更新
external_account_relink_required保管トークンを更新できず、連携し直しが必要

フック / SIEM 側でイベント型を allow-list している場合は追加が必要。

保管トークンの取り出しのイベント(external_account_token_retrieved / _retrieval_failed / _refreshed、external_account_relink_required)は、details の account_linking に連携先(provider、連携が見つかった後だけ)と、断った理由(error)を持つ。連携先ごとに数えれば、連携先の不調とクライアントの問題を分けられる。

1.8 ユーザー・テナントの削除時の後始末​

🟢 追加。 ユーザーを削除すると、ユーザーのライフサイクル処理(linked-external-account-deletion)が、そのユーザーの連携と連携セッションを削除する。削除の前に、保管しているトークンと、確定されていない連携セッションが預かっているトークンを、連携先の revocation_endpoint で失効させる。失効はベストエフォートで、失敗しても行は削除する。連携先の設定を無効化(enabled: false)していても失効させる。

連携が無いユーザーの削除は変わらない。

テナントを削除するときも、削除の前にテナントの全ての連携と、確定されていない連携セッションのトークンを失効させてから行を消す。行はテナントへの外部キーの ON DELETE CASCADE でも消えるが、それでは失効されない。失効は同期的に行い、連携 1 件ごとに外部IdPを呼ぶので、削除の応答までの時間は連携の数に比例する。dry_run=true では失効しない。

1.9 delete-expired-data の内訳​

🟡 挙動変更。 POST /v1/admin/operations/delete-expired-data が期限切れの連携セッションも削除し、レスポンスの内訳に account_linking_session が加わった。

{ "deleted": { "...": 0, "account_linking_session": 3 }, "total_deleted": 3 }

確定されなかったセッションが預かっていた暗号化トークンも、行と一緒に消える。外部IdPでの失効はしない。件数を固定長で扱っている自動化は影響を受ける。

1.10 付属の認可画面(app-view)​

🟡 挙動変更。 入力した識別子(メールアドレスなど)とユーザーの状態を保存する sessionStorage のキーを、認可リクエストの ID ごとにした。

従来修正後
authIdentifierauthIdentifier:<認可リクエストの ID>
authUserStatusauthUserStatus:<認可リクエストの ID>

同じタブで続けて別のフロー(別テナントを含む)を開いたとき、前のフローで入力したメールアドレスが次のフローのログインフォームに入らなくなった。同じフローの中での再読み込みでは、これまでどおり入力が戻る。従来のキーは読まれない。

🟢 追加。 同意画面に、新しいスコープの表示名を追加した。

スコープ表示
linked_account:readSee which external accounts you have linked
linked_account:manageLink and unlink your external accounts
linked_account:read_tokenUse your linked external accounts

1.11 FederationConfigurationQueryRepository のメソッド追加​

⚙️ 無効化した連携先の設定も読めるように、FederationConfigurationQueryRepository に get(Tenant, FederationType, SsoProvider, Class<T>, boolean includeDisabled) を追加した。default 実装は無い。

同梱の実装(FederationConfigurationQueryDataSource)は対応済みで、既存の get(...) の挙動は変わらない。このインターフェースを自前で実装している構成は、追加したメソッドの実装が要る。

1.12 設定例の変更​

⚙️ config/examples/standard-oidc-web-app/ に連携用の設定例(federation-account-linking.json など)を追加した。あわせて public-client.json を次のように変えた。

  • token_endpoint_auth_method を none から client_secret_basic に変えた(保管トークンの取得は public クライアントを拒否するため)
  • scope に linked_account:read_token / linked_account:read / linked_account:manage、extension に linking_return_uris / linking_providers / linking_token_providers を追加した

public-tenant.json の scopes_supported にも同じスコープを追加した。この設定例を入れ直したローカル環境では、クライアントがシークレットで認証するようになる。

1.13 保管トークンの暗号化の鍵と運用API​

🟢 追加。 保管トークンは、テナントごとのデータ鍵(DEK)を鍵暗号化鍵(KEK)で包む 2 段の鍵で暗号化する。KEK を替えても DEK を包み直すだけで、保管トークンは書き換えない。

追加したもの内容
環境変数 ENCRYPTION_KEYSKEK の束。<key id>:<base64 の鍵> をカンマ区切り。未設定なら ENCRYPTION_KEY を key id default として使う
環境変数 ENCRYPTION_ACTIVE_KEY_ID新しく包むのに使う KEK の key id。ENCRYPTION_KEYS を設定したときは必須
GET /v1/admin/operations/data-keysKEK ごとの DEK の件数、包み直し待ちの件数、この台が持たない KEK で包まれた件数
POST /v1/admin/operations/data-keys/rewrapactive でない KEK で包まれた DEK を包み直す(expected_active_key_id 必須、max_number は 1〜10000、既定 1000)。応答した台の active な KEK が expected_active_key_id と違えば 409
POST /v1/admin/operations/data-keys/rotateテナントの DEK の新しい世代を作る(tenant_id、purpose)
GET /v1/admin/operations/data-keys/listテナントをまたいで DEK を一覧する(tenant_id・kek_id・status・purpose で絞り込み、limit / offset、total_count)。各行の kek_configured は応答した台がその KEK を持つか
GET .../tenants/{tenant-id}/data-keys(管理API、idp:data-key:read)テナントの DEK を世代ごとに一覧する。保管トークンの DEK には、その DEK で暗号化している連携の数が付く
POST .../tenants/{tenant-id}/data-keys/rotate(管理API、idp:data-key:rotate)テナントの DEK の新しい世代を作る。dry_run に対応

どの API も、KEK で包んだ DEK そのもの(wrapped_key)は返さない。管理API はシステムレベル(/v1/management/tenants/{tenant-id}/...)と組織レベル(/v1/management/organizations/{org-id}/tenants/{tenant-id}/...)の両方にある。権限のレコードの作り方は 1.6 と同じ。

ENCRYPTION_KEYS の書き方を誤ると起動に失敗する。条件と交換の手順は データ鍵と鍵暗号化鍵のローテーション を参照。

oauth_token の暗号化は変わらず ENCRYPTION_KEY を直接使う。