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

連絡先変更ポリシー

この文書の目的​

利用者が自分でメールアドレスや電話番号を変えられるようにしたとき、それが「属性の更新」なのか「ログイン識別子の移動」なのかはテナントによって変わります。同じ操作が、あるテナントでは軽く、別のテナントでは重い。

その差を扱うのが連絡先変更ポリシーです。IDポリシーの一部として設定します。


同じ操作が、テナントによって別の重さになる​

identity_unique_key_type は preferred_username(ログイン識別子)をどの属性から導くかを決めます。連絡先を変えたとき何が起きるかは、そこに依存します。

identity_unique_key_typeメールアドレス変更電話番号変更
EMAIL / EMAIL_OR_EXTERNAL_USER_ID識別子が移動する属性の更新
PHONE / PHONE_OR_EXTERNAL_USER_ID属性の更新識別子が移動する
USERNAME 系 / EXTERNAL_USER_ID属性の更新属性の更新

識別子が移動すると、確定後は旧い値でログインできなくなります。アカウントへの入口そのものが変わるということです。

一方「属性の更新」は、クレームの値が変わるだけでログインには影響しません。

同じ POST /v1/me/email/change が、片方では住所変更で、片方では鍵の掛け替えです。ひとつの設定で縛ると、どちらかが過剰か不足になります。だからポリシーは 2 つに分かれています。

contact_change_policy
├── identifier_move ログイン識別子が動く場合
└── attribute_only 属性だけが変わる場合

なぜ識別子の移動を重く扱うのか​

確認コードが証明しているものを見る​

変更フローでは、確認コードは新しい値に送られます。到達性を証明させるためなので当然です。

しかしそれが証明するのは「新しい値を支配していること」であって、「現在のアカウントを支配していること」ではありません。後者はアクセストークンを持っていることだけで示されています。

証明されること            証明されないこと
新しい値に届く → その人が本来の持ち主である

アクセストークンが漏れた場合、攻撃者は自分の連絡先を新しい値に指定できます。コードは攻撃者に届き、確定し、識別子が移動して、本来の持ち主はログインできなくなります。

身元確認との衝突​

eKYC を経たアカウントでは、外部の確認プロバイダが主張したクレームを持っています。

idp-server はその結果でさえ preferred_username を書き換えません。確認結果からユーザー属性へ反映できるのは標準クレームだけで、識別子とライフサイクル状態はテナント設定でも触れないように除外されています。

identifiers and lifecycle state must never be patchable from verification results,
even by tenant configuration.

外部の身元確認プロバイダの結果でも動かさないものを、セルフサービスがワンタイムコード 1 本で動かしてよいか——という問いが残ります。テナントが答えるのは authentication_conditions です(後述)。


変更を許すか、条件を付けるか​

allowed はその区分の変更そのものを止めます(既定 true)。条件を書くまでもなく 「このテナントでは自分で変えさせない」と決められる、いちばん粗い軸です。

止めるのは変更だけで、確認は通ります。確認はアカウントが既に持っている値に *_verified を 立てるだけで、値を置き換えません。「変更させない」は確認について何も言っていないので、 同じ設定で確認まで塞ぐと、変更を禁じたいだけのテナントがメール・電話の確認手段ごと失います。

以下の 2 つの軸は、変更にも確認にも掛かります。確認も *_verified を書くため、そこに直近の 認証や強い認証を要求したいテナントがあり得るからです。

ただし確認が読むのは常に attribute_only の側です。区分を分けているのは識別子が動くかどうか であって、確認は何も動かさないからです。EMAIL テナントでメールの確認に条件を掛けたいなら、 書く先は identifier_move ではなく attribute_only になります。


ポリシーの 2 つの軸​

1. どんな認証を経ていることを要求するか​

authentication_conditions で、アクセストークンの背後にある認証に条件を付けます。

認証方式を名指ししません。 これが設計上いちばん大事な点です。

「現在のパスワードを入力させる」という発想は自然ですが、それはパスワードを持っている利用者にしか通じません。passkey だけで運用しているテナント、外部 IdP からしか入ってこない利用者には、入力すべきパスワードが存在しません。名指しした瞬間、その方式を使っていないテナントが締め出されます。

必要なのは「本人が、最近、認証していること」であって、その方式ではありません。そこで amr(認証方式の記録)に対する条件として書きます。

「password で認証していること」 または 「fido-uaf で認証していること」

テナントは自分のところの利用者が実際に持っている方式を書けばよく、idp-server 側が方式を決め打ちしません。

2. 認証からどれだけ経っていてよいか​

max_auth_age_seconds は、認証してからの経過時間に上限を設けます。OIDC の max_age と同じ考え方です。

長期のアクセストークンやリフレッシュで延命されたトークンでは、「認証したのは数週間前」ということが起こります。識別子を動かす操作にそれを許すかはテナントの判断です。

身元確認の状態は、専用の軸ではなく条件で書く​

「身元確認済みだけに許す」も「身元確認済みには許さない」も、どちらも実在する要求です。専用の設定項目にすると 2 値では片方しか言えず、値を足しても次の要求で詰まります。

authentication_conditions は認証ポリシーと同じ $.user.* を読めるので、どちらも同じ書き方で表せます。

// 身元確認済みだけが識別子を動かせる
{ "path": "$.user.status", "operation": "eq", "value": "IDENTITY_VERIFIED" }

// 身元確認済みには動かさせない
{ "path": "$.user.status", "operation": "ne", "value": "IDENTITY_VERIFIED" }

条件式なので、認証の強さと組み合わせることもできます(グループ内は AND)。

IDENTITY_VERIFICATION_REQUIRED は「未確認」です。 「確認が要求されている」状態であって、済んでいる状態ではありません。authentication_device_rule.required_identity_verification: true のテナントでは端末登録の時点で全利用者がこの状態になるため、IDENTITY_VERIFIED と同一視すると、端末必須のクライアントでは変更できる利用者が居なくなります。確認が実施中かどうかは利用者の status ではなく、身元確認申込み側で持ちます。


防ぐ層と、気づかせる層​

ここまでは変更を防ぐ話ですが、防ぎきれなかった場合の層が別に要ります。

コードは新しい値に届くので、フローの間、本来の持ち主には何も届きません。気づく契機がゼロです。

notify_previous_value は、確定後に置き換えられた側へ「変更されました」を送ります。

変更フロー                     宛先
1. 変更を開始 新しい値 ← 確認コード
2. コードを検証 —
3. 確定 旧い値 ← 変更通知

これは変更を防ぎません。乗っ取りから発覚までの時間を縮めるための層です。防止(authentication_conditions)と検知(notify_previous_value)は目的が違うので、片方があればもう片方が要らない、という関係ではありません。

通知に載せる新しい値は部分マスクにします。乗っ取り後はその受信箱を第三者も読んでいる可能性があるため、本人が「自分のではない」と判別できる分だけを載せます。


デフォルトの考え方​

未設定のテナントで効くのは 1 つだけです。

identifier_moveattribute_only
allowedtruetrue
authentication_conditionsなしなし
max_auth_age_secondsなしなし
notify_previous_valuetruetrue

authentication_conditions をデフォルトで空にしているのは、前述のとおりデフォルトで方式を名指しすると、その方式を使っていないテナントを締め出すからです。ここはテナントが自分の構成を知って書くべき箇所です。

デフォルトに入れたのは、方式にも利用者の状態にも依存せず安全に仮定できるもの——置き換えた事実を旧い値に伝える(検知の層)——だけです。

裏を返すと、既定のままではスコープだけが門番です。識別子が動くテナントは authentication_conditions と max_auth_age_seconds を明示的に書いてください。

attribute_only を緩いままにしているのは、これが本当にただの属性更新だからです。


関連ドキュメント​