プロンプトと適用コンテキスト
消費者向けWebアプリケーションのセルフサービス型メールリンクパスワードリセットフローを設計してください。リクエストAPIはメールアドレスを受け取り、一致するアカウントが存在する場合、不透明なトークンを含むHTTPSリンクを送信します。トークンは作成後30分で期限切れになります。データベースには生のbearerトークンを決して保存してはなりません。
この設計では、未認証の呼び出し元が特定のメールアドレスが登録されているかを知ること、1つの受信トレイをフラッディングすること、使用済みリンクをリプレイすること、他のユーザーのパスワードを変更すること、または2回目のリセット送信との競合に勝つことを防ぐ必要があります。リセットが成功すると、パスワードが変更され、そのアカウントの未処理のリセットトークンがすべて無効化され、既存の認証済みセッションが取り消され、セキュリティ通知が送信されます。パスワードリセットによって、登録済みのMFA認証要素が削除または置換されることはありません。
これは主にバックエンドのセキュリティと整合性に関する質問です。洗練された回答は、脅威モデルをAPIコントラクト、トークンのライフサイクル、データベーストランザクション、メール配信、セッションモデル、および運用上の証跡と結びつけます。ランダムなトークンと有効期限を挙げるだけでは不十分です。難しい部分は、すべての観測可能なパスと並行パスが同じセキュリティコントラクトに従うようにすることです。
面接官が評価するポイント
最初のシグナルは脅威モデリングです。候補者は、アカウント列挙、リセットメール爆撃、Hostヘッダーインジェクション、URL漏洩、トークン推測、データベース漏洩、リンクリプレイ、クライアント改ざん、同時消費、セッション窃取、およびMFAバイパスを特定できる必要があります。各脅威は、エンドポイントが安全であるという一般的な主張ではなく、特定の制御策にマッピングされるべきです。
2番目のシグナルは、生のトークンと保存されたベリファイアの区別です。メールには高エントロピーのbearerシークレットが含まれます。データベースには一方向ダイジェストのみが保存されるため、トークンテーブルを読み取っても使用可能なリンクが直接漏洩することはありません。候補者はまた、このランダムトークンと人間のパスワードを区別する必要があります。ユーザーが選択したパスワードには低速なパスワードハッシュが必要ですが、均一にランダムな256ビットトークンは、推測可能になることなく高速な暗号ダイジェストによってインデックスを作成できます。
3番目のシグナルはトランザクション設計です。アプリケーションコード内でused_atをチェックし、後で更新すると、リプレイ競合が発生します。同じユーザーに対して2つの異なる有効なトークンが存在すると、アカウントがシリアル化ポイントでない限り、別の競合が発生します。強力な回答では、ユーザー行をロックし、提示されたトークンを条件付きで消費し、パスワードを変更し、兄弟トークンとセッションを取り消し、1つの短いトランザクション内でoutboxイベントを記録します。
4番目のシグナルはリカバリ境界の判断です。忘れたパスワードのリセットは、MFAを無効にしたり、紛失した第2要素を置き換えたりする許可ではありません。より高い保証レベルのアカウントリカバリには、個別に設計された身元確認パスが必要です。また、回答ではセキュリティの質問を唯一のリカバリメカニズムとして拒否し、古いパスワードや新しく生成されたパスワードをメールで送信することを避けるべきです。
最後のシグナルは運用上の考慮です。タイミング、レート制限の動作、ログ、アナリティクス、またはサポートツールがアカウントの存在や生のトークンを開示してしまう場合、汎用的なレスポンスは効果がありません。メール配信は、アカウントを途中で変更することなくプロバイダーの障害に耐える必要があり、モニタリングはログにbearerシークレットを保存することなく不正利用を検出する必要があります。
回答前に確認すべき質問
- これはパスワードの再設定ですか、それとも完全なアカウントリカバリですか? ユーザーが検証済みメールへのアクセス権を保持しており、パスワードのみを紛失した場合は、リンクによってそのパスワードを置き換えることができます。メールアクセス、MFA、およびリカバリコードを紛失した場合は、より強力で独立したリカバリプロセスが必要です。
- アカウントはMFAを使用していますか? パスワードリセットでは、登録済みの要素をそのまま維持する必要があります。プロダクトが両方を回復する単一のフローを希望する場合、その保証要件と不正使用レビューは大幅に変更されます。
- どのセッションを取り消す必要がありますか? このプロンプトではすべてのセッションを取り消します。開始したデバイスのログイン状態を維持するには、そのデバイスがすでに信頼されていたことの証明と、明確に文書化された例外が必要になります。
- 複数のリンクを同時に有効にしておくことは可能ですか? 上限を設けた少数のリンクを許可することで、後から送信されたメッセージが先に届いたという理由だけで正当なメールが無効になるのを防ぎます。成功したいずれかのリンクは、そのユーザーの他のすべてのリンクを取り消す必要があります。
- どのようなリスクと配信チャネルが適用されますか? メールは低リスクの消費者向けアカウントには許容されるかもしれませんが、金融機関や規制対象のアクセスには不十分です。SMS、リカバリコード、サポートによる本人確認、および待機期間には、それぞれ異なる乗っ取りリスクと可用性リスクがあります。
- サインインにはどのようなパスワードポリシーがすでに適用されていますか? リセットでも同じ長さ、ブロックリスト、ハッシュポリシーを使用する必要があります。リセット専用の弱いポリシーは、認証バイパスの原因になります。
- どのような不正使用対策バジェットとメールプロバイダー制限が存在しますか? レート制限には、アカウント、IP、デバイス、グローバルなプロバイダー容量などのディメンションが必要です。アカウントのみのハードロックを設定すると、攻撃者が既知の被害者へのリカバリを拒否できるようになってしまいます。
30秒の回答フレームワーク
「すべてのメールに対して同じ202レスポンスとメッセージを返し、階層化されたレート制限の背後で非同期に検索と配信を実行します。実在するアカウントの場合、32バイトのランダムデータを生成し、設定されたHTTPSオリジンから構築されたリンク内のbase64urlトークンをメール送信し、そのダイジェストのみをユーザー情報および30分の有効期限とともに保存します。GETページでトークンを消費することはありません。最後のPOSTでトークンを再検証し、新しいパスワードをハッシュ化し、ユーザー行をロックし、トークンをアトミックに使用済みにマークし、パスワードを更新し、他のすべてのリセットトークンとセッションを取り消し、通知および監査のoutboxイベントを書き込みます。これにより、同時リクエストやリプレイされたリクエストは条件付き消費に失敗します。MFAのリカバリは別フローとして分離します。」
ステップバイステップの詳細解説
まず、2つのパブリックエンドポイントと意図的に最小限に抑えたレスポンスコントラクトから始めます。
POST /password-reset-requests
{ "email": "person@example.com" }
202 Accepted
{ "message": "If an account matches, reset instructions will be sent." }
POST /password-resets
{ "token": "opaque-base64url-value", "new_password": "..." }リクエストエンドポイントは、アカウントが存在するかどうかにかかわらず、同じステータス、メッセージ形式、およびキャッシュポリシーを返します。データベースの明らかな即時終了やSMTP呼び出しがタイミングオラクルを作成しないように、戻る前にバインドされたキューに処理を渡す必要があります。固定のスリープを追加して問題を解決したとみなしてはなりません。キューの飽和、アカウント専用のスロットル、および異なるエラーパスによって動作が公開されたり、サービス拒否ツールになったりする可能性があります。
メールアドレスの正規化は、プロダクトの検証済みIDルールに従ってのみ行ってください。ドメインを小文字にすることは有効な場合がありますが、すべてのドメインに対してドットの削除やプラス記号タグの処理などのプロバイダー固有のルールを適用すると、2つの異なるアカウントが統合されてしまう可能性があります。正規化されたアカウントキー、送信元IPまたはネットワーク、デバイスまたはリスクシグナル、およびメールプロバイダーのグローバルバジェットにわたってレート制限を階層化します。疑わしいトラフィックはチャレンジにエスカレーションします。パブリックレスポンスは汎用的なままであり、パスワード忘れのリクエストによってアカウントがロックされたりパスワードが変更されたりすることはありません。
既存のアカウントに対しては、暗号学的に安全な乱数生成器を使用して32バイトを生成します。これは256ビットに相当し、パディングなしのbase64urlでは43文字で表現されます。これは設計上の選択であり、普遍的な有効期限やエンコーディングのルールではありません。SHA-256(raw_token)を保存し、生の値をメールリンク経由でのみ送信します。入力は均一にランダムでエントロピーが高いため、高速な一方向ダイジェストでインデックス付きの検索が可能です。サーバーが入力を再度ハッシュ化するため、ダイジェスト自体を送信されたbearer値として使用すると失敗します。パスワードは低エントロピーの人間のシークレットであるため、代わりに低速でソルト付きのパスワードハッシュが必要です。
PostgreSQLテーブルの例は以下のとおりです。
CREATE TABLE password_reset_tokens (
id uuid PRIMARY KEY,
user_id uuid NOT NULL REFERENCES users(id),
token_digest bytea NOT NULL UNIQUE,
created_at timestamptz NOT NULL,
expires_at timestamptz NOT NULL,
used_at timestamptz,
revoked_at timestamptz
);
CREATE INDEX password_reset_tokens_active_user_idx
ON password_reset_tokens (user_id, expires_at)
WHERE used_at IS NULL AND revoked_at IS NULL;メールのURLには、信頼できないHostヘッダーではなく、設定済みまたは許可リストに登録されたオリジンを使用する必要があります。HTTPSを使用し、アプリケーション、プロキシ、アナリティクス、およびエラーログからトークンを秘匿化(マスキング)し、リセットページからサードパーティのアセットを排除します。ナビゲーションによってクエリトークンが開示されないように、no-referrerポリシーを設定します。GETリクエストはフォームを表示したりリンクが無効であることを通知したりできますが、トークンを消費してはなりません。メールセキュリティスキャナーやリンクプレビュー機能は、ユーザーよりも先に日常的にリンクにアクセスするためです。
そのGET中に実行された検証を信頼してはなりません。改ざんされたクライアントは最終エンドポイントを直接呼び出すことができるため、パスワードを変更するPOSTリクエスト側でトークンを受け取り、再検証する必要があります。トランザクションを開く前に、トークンをダイジェスト化し、有効期限内の候補を見つけ、新しいパスワードを検証し、その低速なパスワードハッシュを計算します。これにより、負荷の高いパスワードハッシュ計算処理とほとんどの無効なリクエストをアカウントロックの外側に留めることができます。このエンドポイントにもレート制限を適用してください。
最終的な状態変更では、ユーザー行をシリアル化ポイントとして使用します。
candidate = find token by SHA-256(raw_token)
reject publicly if candidate is missing, expired, used, or revoked
new_hash = Argon2id(new_password, fresh_salt, tuned_parameters)
BEGIN
SELECT id FROM users WHERE id = candidate.user_id FOR UPDATE
UPDATE password_reset_tokens
SET used_at = now()
WHERE id = candidate.id
AND used_at IS NULL
AND revoked_at IS NULL
AND expires_at > now()
RETURNING user_id
if no row returned: ROLLBACK and reject
UPDATE users
SET password_hash = new_hash,
password_changed_at = now(),
auth_version = auth_version + 1
WHERE id = candidate.user_id
UPDATE password_reset_tokens
SET revoked_at = now()
WHERE user_id = candidate.user_id
AND id <> candidate.id
AND used_at IS NULL
AND revoked_at IS NULL
DELETE FROM sessions WHERE user_id = candidate.user_id
INSERT security_outbox(password_reset_succeeded, user_id, occurred_at)
COMMITBEGINの前にハッシュを計算しますが、条件付きトークン更新が成功しない限りコミットしないでください。新規デプロイの場合、OWASPは現在、19 MiBのメモリ、2回の反復、並列度1のArgon2idを最小構成の1つとして挙げています。ベンチマークを実施し、正当な認証キャパシティを安全に保ちながらコストを引き上げてください。パスワードハッシュとともにアルゴリズムとパラメータを保存し、それらを将来変更できるようにします。Bcryptは入力長に制限があるレガシーなフォールバックであり、直接の同義語ではありません。
行ロックは2種類の競合を処理します。2つのリクエストが同じトークンを提示した場合、最初の条件付き更新のみがused_atを設定できます。1人のユーザーに対して2つの異なる有効なトークンを提示した場合、両方のリクエストが初期読み取りとハッシュ処理を通過する可能性がありますが、ユーザーロックを保持できるのは1つだけです。そのトランザクションがもう一方のトークンを取り消すため、2番目のリクエストがロックを取得したときには、その条件付き更新は行を返しません。したがって、パスワードには1つの勝利した値が存在し、競合に負けたすべてのリクエストは終端のトークン状態を観測します。
サーバー側でのセッション削除により、データベースに紐づくセッションを即座に無効化できます。リフレッシュトークンについては、サーバー側のレコードを取り消します。それ以外のステートレスなアクセストークンの場合、auth_versionをインクリメントするかpassword_changed_atをチェックすることは、保護されたすべてのリクエストがそのサーバー状態を比較する場合にのみ機能します。そのようなチェックがない場合、失効はトークンの有効期限が切れるまで遅延します。ブラウザのCookieを削除すれば他の場所の攻撃者も失効すると主張するのではなく、その制限を明確にしてください。
成功通知とセキュリティ監査イベントを同じトランザクション内のoutboxを介して書き込み、コミット後に配信します。通知には日時、アカウントリカバリのガイダンス、不正を報告する方法が含まれ、古いパスワードや新しいパスワードは決して含まれません。通知プロバイダーの停止時はリトライとアラートをトリガーする必要があります。すでに変更されたパスワードをロールバックしてはなりません。リクエスト側では、データベースのコミット後にキューが失敗してもリクエストが暗黙のうちに取り残されないように、トークンとメールジョブを永続的に関連付ける必要があります。outboxまたは1つのトランザクションジョブストアがその境界を解決します。
リクエストごとに以前のリンクを取り消すのではなく、有効なリンクの数を制限して複数許可します。即時置換を許可すると、攻撃者がリセットを繰り返し要求し、被害者がメールを開く前に最新のメールを無効化できるようになります。未処理の行数を制限し、上限に達したときに最も古い行を取り消すか期限切れにし、成功後に残りのすべての行を取り消します。監査保持ポリシーに従って、終端状態の行および期限切れの行を定期的に削除します。
不透明な保存トークンは、通常、自己完結型のJWTリセットリンクよりもシンプルです。署名付きJWTであっても、即時の1回限りの使用、ユーザー全体での取り消し、およびパスワード変更の競合のためにサーバー側の状態が必要です。その状態が存在するならば、ランダムトークンとダイジェストの組み合わせのほうがクレームやパースパスが少なくなります。短いPINは手入力には便利ですが、エントロピーがはるかに低いため、厳格な試行回数制限と検証後の限定的なリセットセッションが必要です。
モニタリングでは、リスクディメンションごとのリクエストレート、キューの遅延、プロバイダーエラー、配信結果、トークン検証の失敗、成功時のトークン経過時間、リプレイ試行、およびセッション失効の失敗を追跡する必要があります。生のメールアドレス、トークン、新しいパスワード、および完全なリセットURLをメトリクスやログに記録しないでください。アラートは、パブリックレスポンスでアカウントの存在を公開することなく、グローバルな攻撃キャンペーンと単一アカウントへのフラッディングの両方を検出できる必要があります。
フローを単なる正常系エンドポイントとしてではなく、ステートマシンとしてテストします。同一トークンを使用した並行POST、および同一ユーザーに対する2つの異なるトークンを使用した並行POSTを送信します。成功するのは正確に1つだけでなければなりません。境界値での有効期限切れ、成功後の再利用、兄弟トークンの取り消し、存在しないアカウント、キューおよびメールの停止、Hostヘッダーの改ざん、Refererの漏洩、ログの秘匿化、レート制限のディメンション、別デバイスでのセッション拒否、通知のリトライ、およびMFAが設定されたアカウントを検証します。セキュリティスキャナーがトークンを消費せずにリンクをGETできる必要があります。
高品質な回答例
「私はリセットトークンを一時的な認証要素として定義し、そのライフサイクル全体を中心として設計します。リクエストエンドポイントは常に同じ202レスポンスを返します。アカウント、ネットワーク、デバイス、およびプロバイダーレベルの不正使用対策の背後で検索と配信をキューに入れますが、誰かがメールアドレスを入力したというだけでアカウントをロックしたり変更したりすることはありません。
一致するアカウントに対しては、32バイトのランダムデータを生成し、HTTPSリンク内でbase64urlの値を送信し、そのSHA-256ダイジェスト、ユーザーID、および30分の有効期限のみを保存します。リセットオリジンはHostから取得するのではなく事前設定されており、ページにはサードパーティのリソースがなく、no-referrerポリシーを使用し、すべてのログパイプラインがトークンを秘匿化します。メールスキャナーがリンクを開く可能性があるため、GETリクエストはフォームの表示のみを行います。
最後のPOSTでは、短いトランザクションを開始する前にトークンを再度検証し、新しいパスワードをハッシュ化します。トランザクション内では、ユーザー行をロックし、そのトークンを条件付きで使用済みにマークし、パスワードと認証バージョンを更新し、すべての兄弟リセットトークンとサーバー側セッションを取り消し、outboxイベントを挿入します。条件付き更新により同一トークンのリプレイが防止され、ユーザーロックと兄弟の取り消しにより、2つの異なるトークンがシリアル化されて勝者が1つに決定されます。
コミット後、ワーカーがセキュリティ通知を送信し、独立してリトライを行います。汎用レスポンスとタイミング、トークンおよびURLの漏洩、同時使用、有効期限、プロバイダーの障害、リモートセッションの拒否、およびMFAアカウントをテストします。パスワードをリセットしてもMFAは維持されます。すべての認証要素を紛失した場合は、別途用意された保証レベルの高いリカバリプロセスを経由します。」
よくある間違い
- 「メールアドレスが見つかりません」を返す → 攻撃者が登録済みアカウントを列挙できるようになる → 同一のパブリックステータスとメッセージを返し、即時終了するタイミングパスを避ける。
- 生のトークンを保存する → データベースを読み取った攻撃者が有効なリセットリンクを入手できる → 一方向ダイジェストを保存し、配信とユーザー送信時を除き、すべての場所で生の値を秘匿化する。
- 最終フォームのユーザーIDを使用する → クライアントが対象アカウントをすり替えることができる → サーバー側のトークンレコードからのみユーザーを特定する。
- GETリクエストでリンクを消費する → ユーザーがアクセスする前にメールスキャナーが無効化してしまう可能性がある → パスワードを変更するPOSTリクエストの処理中にのみ消費する。
- GETで検証しPOSTで検証しない → 改ざんされたクライアントが表示されたフォームをバイパスする → 最終的なサーバー側トランザクション内で有効期限と終端状態を再検証する。
- 条件なしでチェックしてから更新する → 並行リクエストが両方ともチェックを通過する可能性がある → トランザクション内で条件付き更新を使用し、ユーザー行で異なるトークンをシリアル化する。
- リクエストごとに以前のリンクを無効化する → 攻撃者が被害者の最新メールを常に古い状態にし続けることができる → 上限付きのセットを許可し、1つが成功した後にのみすべての兄弟を取り消す。
HostからURLを構築する → ポイズニングされたリクエストにより、攻撃者が制御するリセットドメインが送信される可能性がある → 設定済みまたは許可リストに登録されたHTTPSオリジンを使用する。- ログやアナリティクスにトークンを含める → オブザーバビリティシステムが認証情報ストアになってしまう → クエリ文字列を秘匿化し、リセットページでのサードパーティリソースを禁止する。
- セッションポリシーなしでリセット後に自動ログインする → セッション固定化やセッション窃取の動作が不明確になる → 通常のログインを要求し、サーバー側セッションを明示的に取り消す。
- パスワードと一緒にMFAをリセットする → 1つのメールを乗っ取るだけで強力な要素が無効化されてしまう → MFAリカバリはリスクベースの別フローとして維持する。
- 新しいパスワードをメールで送信する → 安全でないチャネルにパスワードが残り続け、攻撃者が被害者を締め出すことができる → 期限付きリンクを送信し、有効な証明が提出されるまで何も変更しない。
フォローアップの質問と回答
フォローアップ1:プロダクトがステートレスなJWTアクセストークンを使用している場合、何が変わりますか?
サーバー側でリフレッシュトークンを取り消します。アクセストークンを即座に無効化するには、認証バージョンまたはissued-at値を含め、保護されたリクエストごとに現在のサーバー状態と比較します。アーキテクチャがその検索または失効キャッシュを拒否する場合、古いアクセストークンは有効期限が切れるまで有効なままになります。即時ログアウトを約束するのではなく、最大の露出リスクを提示してください。
フォローアップ2:新しいリクエストによって、以前のすべてのリセットリンクを無効化すべきですか?
通常、成功する前には無効化しません。メールは遅延したり順序が入れ替わったりする可能性があり、アドレスを知っている攻撃者が被害者の最新リンクを継続的に無効化する恐れがあります。期限切れでないトークンの小規模で上限付きのセットを保持し、リクエスト量を制御し、パスワード変更が成功したトランザクション内ですべての兄弟トークンを取り消します。高リスクなプロダクトではより厳格な置換を選択することもありますが、その可用性のトレードオフを受け入れる必要があります。
フォローアップ3:URLトークンの代わりに6桁のコードをサポートするにはどうすればよいですか?
6桁のコードは256ビットのトークンよりもはるかにエントロピーが低くなります。これをアカウントおよび範囲が厳密に制限されたリセット試行にバインドし、サーバー側で試行回数制限と有効期限を適用し、検証が成功した後にのみ短命のリセットセッションを作成します。検証済みコードが一般的な認証済みセッションにならないようにし、クライアントがユーザーIDを選択できないようにしてください。
フォローアップ4:どのような新しいパスワードルールを適用しますか?
サインインと同じベリファイアポリシーを使用します。プロダクトが現在のNISTガイダンスを採用している場合、単一要素として使用されるパスワードは最低15文字です。MFA内でのみ使用されるパスワードは最低8文字の場合があります。少なくとも64文字を許可し、ブロックリストを使用して一般的または侵害された値を拒否し、任意の文字種要件や定期的な変更ルールを追加しないでください。パスワードハッシュのコストは、これらの入力ルールとは別に調整します。
フォローアップ5:ユーザーがメールとMFAの両方へのアクセスを失った場合はどうなりますか?
それは完全なアカウントリカバリであり、このパスワードリセットエンドポイントの対象外です。アカウントのリスクに応じて、事前登録されたリカバリコードまたは連絡先、残りの認証要素、反復的な身元確認、待機期間、および手動レビューを使用します。リカバリアドレス自体の変更には、検証と独立した通知が必要です。簡単に調査できるセキュリティの質問を唯一の証明としてフォールバックしてはなりません。
フォローアップ6:アカウント列挙が実際に制御されていることをどのように検証しますか?
ステータス、ボディ、ヘッダー、キャッシュの動作、レイテンシ分布、レート制限の遷移、およびダウンストリームの副作用にわたって、実在するアカウントと存在しないアカウントを比較します。単に静かなローカル環境での実行だけでなく、キューの飽和やプロバイダーの障害条件下でテストします。CDN、プロキシ、アプリケーション、アナリティクス、およびサポートログを確認して識別子や完全なURLがないかを検証し、不正使用ダッシュボードが集計シグナルを公開しつつアカウント存在確認サービスになっていないことを確認します。