ユーザーに信頼され、受信トレイに届くパスワードリセットメールの作り方

安全なパスワードリセットの仕組みを構築する方法。1回限りのトークン、短い有効期限、アカウント列挙の防止、わかりやすいメールテンプレート、変更後の確認メールまで解説します。

パスワードリセットのメールは、あなたのプロダクトのマスターキーです。それを読める人なら誰でもアカウントを乗っ取れます。だからこそ攻撃者に狙われ、ユーザーも警戒するようになっています。

優れたリセットの仕組みには、攻撃者に耐えられる安全性と、正規のユーザーが数秒で信頼できるわかりやすさの両方が必要です。このガイドでは、誰かが「パスワードをお忘れですか?」をクリックした瞬間から、変更後の確認メールまで、その両方を順に解説します。

全体の流れ

  1. ユーザーがパスワードをお忘れの場合のページでメールアドレスを入力します。
  2. アプリは、アカウントが存在するかどうかにかかわらず、常に同じメッセージを表示します。
  3. アカウントが存在する場合は、1回限りのトークンを作成し、リセットリンクをメールで送ります。
  4. ユーザーがリンクを開いて新しいパスワードを設定すると、トークンは使用済みになります。
  5. ユーザーの他のセッションをサインアウトさせ、「パスワードが変更されました」というメールを送ります。

ステップ1:誰がアカウントを持っているかを明かさない

フォームが送信されたら、常に同じ文面で応答します。

ada@example.com のアカウントが存在する場合は、パスワードをリセットするためのリンクを送信しました。リンクの有効期限は30分です。

代わりに「そのメールアドレスのアカウントはありません」と表示すると、アドレスのリストを持っている人に、そのうちどれがあなたのプロダクトを使っているかを正確に教えることになります。応答時間も同程度に保ちましょう。アカウントの検索とメール送信をバックグラウンドで行い、存在しないアドレスのときだけ目に見えて速く応答することがないようにします。

ステップ2:強力な1回限りのトークンを作成する

リンクに含めるトークンは、長く、ランダムで、推測不可能でなければなりません。

import { randomBytes, createHash } from 'node:crypto';

const token = randomBytes(32).toString('base64url');            // goes in the email
const tokenHash = createHash('sha256').update(token).digest('hex'); // goes in the database

await db.passwordResets.upsert({
  userId: user.id,
  tokenHash,
  expiresAt: new Date(Date.now() + 30 * 60 * 1000), // 30 minutes
});

ルールは次のとおりです。

  • 保存するのはトークンのハッシュだけにし、データベースが漏えいしても有効なリセットリンクが渡らないようにします。
  • 有効なトークンはユーザーごとに1つ:新しいトークンを発行したら、古いものと置き換えます。
  • 有効期限は短く:15〜60分が一般的です。
  • 1回限り:パスワードが変更されたらすぐにトークンを削除します。
  • その間にユーザーがサインインに成功したり、メールアドレスを変更したりした場合は、トークンを無効にします。

ステップ3:すぐにメールを送信する

ユーザーは待っているので、バッチジョブではなくリクエストの中で送信します。

import os
import requests

def send_reset_email(user, token):
    link = f"https://app.yourco.com/reset-password/{token}"
    res = requests.post(
        "https://backend.inbixio.com/api/send-email/",
        headers={"X-API-Key": os.environ["INBIXIO_API_KEY"]},
        json={
            "to_email": user.email,
            "subject": "Reset your YourApp password",
            "message": render_reset_template(name=user.first_name, link=link, minutes=30),
        },
        timeout=10,
    )
    res.raise_for_status()

security@yourco.comのような、専用の認証済みアドレスから送信しましょう。Inbixioでは、そのメールボックスを独立したメールサーバーとして接続し、認証サービスにはそのサーバーに固定したAPIキーを渡します。こうすれば、リセットメールはユーザーが想定しているアドレスから届きます。そのサーバーを削除した場合は、キーを固定し直してください。設定方法はAPIキーのセキュリティをご覧ください。

ステップ4:信頼されるリセットメールを書く

フィッシングメールが最もよくまねるのは、パスワードリセットのメールです。あなたのメールは見間違えようのないものにしましょう。

  • 件名:シンプルで具体的に。例:「Reset your YourApp password」。急かす表現や感嘆符は使いません。
  • わかりやすいボタンを1つと、ボタンをブロックするクライアントのためにリンクの全文をテキストで記載します。
  • 有効期限:「このリンクの有効期限は30分です。」
  • 状況:リクエストが行われた日時と、わかる場合はブラウザや都市。
  • 心当たりがない場合の対応:「パスワードのリセットをリクエストしていない場合は、このメールを無視してください。パスワードは変更されません。」
  • 他のリンク、画像、宣伝は入れない:リンクが少ないほど、本物かどうかを確認しやすくなります。
<div style="font-family:system-ui,sans-serif;max-width:520px;margin:auto;color:#111;line-height:1.5">
  <p>Hi Ada,</p>
  <p>We received a request to reset the password for your YourApp account.</p>
  <p style="margin:28px 0">
    <a href="https://app.yourco.com/reset-password/TOKEN"
       style="background:#584efe;color:#fff;padding:12px 20px;border-radius:8px;text-decoration:none;font-weight:600">
      Choose a new password
    </a>
  </p>
  <p>This link expires in 30 minutes and can only be used once.</p>
  <p>If you didn't ask to reset your password, you can safely ignore this email.</p>
  <p style="color:#666;font-size:13px">Button not working? Paste this link into your browser:<br>
    https://app.yourco.com/reset-password/TOKEN</p>
</div>

ステップ5:リンクを慎重に扱う

ユーザーがリンクを開いたら、次のように処理します。

  • トークンのハッシュを検索し、有効期限が切れていないことを確認して「新しいパスワード」のフォームを表示します。この時点ではまだユーザーをサインインさせません。
  • トークンをサードパーティへのリクエストに含めない:リセットページに<meta name="referrer" content="no-referrer">を追加し、アナリティクスや広告のスクリプトを読み込まないようにして、URL内のトークンがリファラーヘッダーから漏れないようにします。
  • 送信時には、新しいパスワードをパスワードルールに照らしてチェックし、低速なアルゴリズム(bcrypt、scrypt、Argon2)でハッシュ化したうえで、トークンを削除し、他のすべてのセッションをサインアウトさせます。

ステップ6:変更を確認する

パスワードが変更されたら、必ず短い確認メールを送りましょう。

YourAppのパスワードが10月28日 14:32 UTCに変更されました。ご本人による変更であれば、これ以上の操作は必要ありません。お心当たりがない場合は、すぐにアカウントを保護してください。

このメールは、他人がアカウントに侵入した場合にユーザーが気づくための警報です。決して省略しないでください。

確実に届くようにする

迷惑メールフォルダーに入ったリセットメールは、サポートへの問い合わせにつながります。リリース前に、次のことを行いましょう。

  • 送信ドメインにSPF、DKIM、DMARCを設定する
  • GmailとOutlookのアドレスでテストし、ヘッダーにPASSと表示されることを確認する
  • デプロイ後は、InbixioダッシュボードのFailed Deliveries(配信失敗)率を見守る

チェックリスト

  • 既存のアドレスにも未登録のアドレスにも同じレスポンスを返す
  • 32バイトのランダムなトークンを使い、保存するのはそのハッシュだけ
  • 有効期限15〜60分、1回限り、有効なトークンはユーザーごとに1つ
  • 専用の認証済み送信元から、すぐに送信する
  • わかりやすいテンプレート:ボタン1つ、テキストのリンク、有効期限、「心当たりがない場合」の一文
  • リセットページにサードパーティのスクリプトがなく、リファラーが無効になっている
  • 変更後に他のセッションをサインアウトさせ、確認メールを送る

Inbixioで送信しよう

お使いのSMTPサーバーを接続し、権限を絞ったAPIキーを作成すれば、HTTPSリクエスト1回であらゆる言語から送信できます。

アカウントを作成 料金を見る