「退会処理」と「不正利用時の強制ログアウト」を、同じ削除エンドポイントで済ませている設計を見直したことはあるでしょうか。この記事は、認証基盤やユーザー管理機能を持つバックエンドを設計・運用しているエンジニアに向けて書いています。
アカウントの停止処理は、一見単純なCRUD操作に見えます。ところが「今すぐ止めるべきこと」と「あとで実行してよいこと」を区別せずに1つの削除処理にまとめると、攻撃者のセッションが生き残ったまま調査証跡だけが消える、という事故につながります。
何が起きるか:2つの時計を1つにした事故
典型的な場面を想定します。サポート担当者に「ブラウザからセッションをコピーされた」という通報が入ります。
攻撃者側はリフレッシュトークン(有効期限の切れたアクセストークンを再発行するための長期トークン)を使い、すでに再認証を試み続けています。同じタイミングで、正規の利用者本人からも「アカウントを閉じてほしい」という退会依頼が届きます。
ここで両方を「ユーザーを削除する」という同一の処理として扱うと、レースコンディション(処理の実行順序によって結果が変わってしまう競合状態)が発生します。
削除処理が完了するまでの間、攻撃者は有効なセッションを保持し続けられる可能性があります。逆に、削除を急ぎすぎると、不正利用を調査するために必要だった操作ログやセッション情報まで一緒に消えてしまいます。
影響範囲は「該当ユーザー1件」では済みません。削除エンドポイントを共通化している設計では、サポート運用フロー・不正調査フロー・データライフサイクル管理のすべてが、同じ1つの実装のバグやタイミング問題に引きずられます。
なぜ起きるか:権限も証跡要件も違う3者を1本の処理にまとめている
原因を分解すると、3つの層で判断主体と要件が違うことが見えてきます。
1つ目は、セッション失効を判断するのは誰か、という点です。不正利用の通報を受けたサポート担当者や不正調査担当者が、緊急性の高い操作として実行します。
2つ目は、退会・削除を判断するのは誰か、という点です。利用者本人の申請を起点に、削除ワーカー(非同期でデータ削除を実行するバッチ処理)が、法務・不正調査・サポート側の保留要件を確認したうえで実行します。
3つ目は、両者が参照する識別子の性質です。メールアドレスは変更可能な「検索用の手がかり」に過ぎず、主キーとして扱うべきではありません。ユーザーID(システム内部で一意に固定される識別子)を安定した主キーとして使う必要があります。
この3つを1つの削除APIに詰め込むと、権限チェックと監査記録が同じトランザクション境界に収まらなくなります。キューの再試行、古いキャッシュの残存、2つ目のブラウザからのアクセスといった「間に割り込む要因」が、削除処理の途中に入り込む余地が生まれます。
加えて、読み取り系のAPI設計も見落とされがちなポイントです。ユーザー一覧の取得と、単一ユーザーの参照は、本来別々の認可ポリシーとキャッシュポリシーを持つべき処理です。
一覧応答は管理者向けの厳格な認可と短いキャッシュ期間が必要ですが、単一ユーザー応答は操作者本人の権限に対して認可すればよいケースがあります。ここを共通化すると、削除済み・無効化済みのプロフィールが、ポリシーで許容される期間より長くキャッシュに残り続けるという副作用が起こります。
自分のプロジェクトが該当するか確認する方法
以下の観点で、自分の実装を点検してみてください。
- 削除系エンドポイントの数:
DELETE /users/{id}のような単一のエンドポイントが、退会・強制停止・不正凍結のすべてを兼務していないか、ルーティング定義やコントローラーのコードを確認する - 主キーの実体:ユーザーの識別に、変更可能なメールアドレスを主キーやパストラメータとして使っていないか、DBスキーマとAPIパスの両方を確認する
- セッション失効の粒度:不正利用対応時に「該当セッション1件」ではなく「そのユーザーの全セッション」を失効させる処理が独立して存在するか
- 削除前のプロフィール状態遷移:物理削除の前に「非アクティブ」のような中間状態を経由しているか、それとも即座に行が消えるか
- 一覧APIと単体APIの認可コード:同じミドルウェアや同じキャッシュキー生成ロジックを共有していないか
該当する項目が1つでもあれば、次章の対策を検討する価値があります。
対策の手順
ステップ1: ユーザーIDを唯一の安定した主キーにする
メールアドレスや電話番号は検索用インデックスとして残しつつ、削除・失効・監査ログの結びつけは必ずユーザーIDで行うようにします。
マイグレーションが難しい既存システムでは、まず監査ログテーブルとセッションテーブルの外部キーがユーザーIDを参照しているか確認するところから始められます。
ステップ2: 破壊的操作の前にプロフィール状態を記録する
物理削除の前に、ビジネスロジック層でステータスを遷移させます。たとえば active → suspended → pending_deletion → deleted のような段階を設け、どの権限レベルの操作者がどの遷移を許可されるかを明示します。
この状態遷移とアプリケーション層での権限チェックは、同一のトランザクション内で記録することが望ましいです。監査記録と認可判断が分かれたトランザクションになっていると、後から「誰がいつ何の権限で遷移させたか」を追跡できなくなります。
ステップ3: セッション失効を独立したAPIとして持つ
不正利用時には、プロフィール状態を変える前に、対象ユーザーの全セッションを失効させる処理を先に呼び出します。
# 概念的な呼び出し順序の例
# 1. 通報を受けたら、まずセッション失効を実行
curl -X POST https://api.example.com/v1/auth/session/revoke_all_for_user/USER_ID \
-H "Authorization: Bearer $OPERATOR_TOKEN"
# 2. 失効が成功したことを確認してから、プロフィール状態を変更
curl -X PATCH https://api.example.com/v1/users/USER_ID/state \
-H "Authorization: Bearer $OPERATOR_TOKEN" \
-d '{"state": "suspended", "reason": "compromise_reported"}'意図的にセッション失効の対象範囲を「そのブラウザ」ではなく「そのユーザー全体」に広げているのは、リスクの単位がブラウザではなく識別子そのものだからです。
ステップ4: 削除は法務・不正調査の保留要件を満たしてから非同期に行う
通常の退会依頼では、削除ワーカーを即時実行せず、承認された削除依頼から実際の削除完了までの間に、法務・不正調査・サポート部門が確認できる保留期間を設けます。
この保留期間の長さはプロジェクトごとに異なりますが、少なくとも「セッション失効のSLO(サービスレベル目標。通報から全セッション失効までの時間)」と「削除ライフサイクルのSLO(承認から削除完了までの時間、保留期間込み)」を別々の指標として定義し、ポリシー担当者が具体的な数値を決める必要があります。
ステップ5: 一覧APIと単体APIの認可・キャッシュを分離する
管理者向けの一覧APIには短いキャッシュ期間と厳格な認可を、単体ユーザー参照APIには操作者本人ベースの認可を、それぞれ別ポリシーとして設定します。
Redisなどでキャッシュしている場合は、削除・凍結イベント発生時にそのユーザーIDに紐づくキャッシュキーを明示的に無効化するフックを、状態遷移の処理に組み込んでおくと安全です。
まとめ
アカウント停止・削除の設計を見直す際は、次の3点を確認してみてください。
- 削除エンドポイントが「不正対応」「通常退会」の両方を1本で兼務していないか、ルーティングを洗い出す
- ユーザーIDを唯一の主キーとして、セッション・監査ログ・削除ワーカーすべてがそれを参照しているか確認する
- セッション失効SLOと削除ライフサイクルSLOを、別々の数値目標として文書化できているか
これらは一度に全部作り直す必要はありません。まずはセッション失効APIを独立させ、プロフィール状態遷移を挟むところから着手できる範囲で始めてみるのがよさそうです。