パスワード再設定機能を実装していると、最初の大きな壁になるのが「メールが届かない」という問題です。
SMTP設定や認証情報を修正し、ようやく再設定メールが届いたときには、「これで完成した」と思いたくなります。
私自身もそうでした。
実際に開発していた業務アプリでは、SMTP設定を修正したことで、パスワード再設定メールそのものは正常に届くようになりました。メール本文にはパスワード再設定用のURLも入っています。
「これなら大丈夫だろう」と思ってリンクをクリックしました。
しかし、表示されたのは新しいパスワードを入力する画面ではありませんでした。表示されたのは、いつものTOP画面でした。
メールは届いています。URLも存在しています。バックエンドでは再設定用のトークンも発行されています。それでも、利用者はパスワードを変更できません。
原因を調べたところ、問題はメール送信側ではありませんでした。フロントエンド側のApp.tsxで、URLに含まれている再設定トークンを検出し、パスワード変更画面へ遷移させる処理が不足していたのです。
バックエンドだけ直しても、機能は完成しません。
さらに、AIを使ったアプリ開発ではもう一つ注意があります。
AIが「パスワード再設定機能を実装しました」「修正は完了しました」と回答しても、その文章を完成条件にしてはいけません。
完成条件にするべきなのは、実際の利用者が本番環境で最後まで操作できることです。
この記事では、「パスワード再設定メールは届くのにリンク先が正常に動かない」という問題を例に、どこから確認すればよいのかを順番に解説します。
結論|メールが届いてもパスワード再設定機能は完成していない

最初に結論をお伝えすると、パスワード再設定は「メールを送る機能」ではありません。
利用者が、パスワード再設定を申請し、メールを受信し、メール内のURLを開き、パスワード変更画面を表示し、新しいパスワードを入力し、パスワードを更新し、新しいパスワードでログインするところまでを含めて、一つの機能です。
メール送信成功=パスワード再設定成功、ではありません。
メール送信は、再設定フローの途中にある一工程にすぎません。
パスワード再設定は複数の処理が連続している
パスワード再設定を技術的な流れにすると、次のようになります。
再設定要求 → トークン発行 → 再設定URL生成 → メール送信 → URLクリック → フロント側でURL・トークンを検出 → パスワード変更画面表示 → 新パスワード送信 → サーバー側でトークン検証 → パスワード更新 → 新パスワードでログイン
一つでも欠ければ、利用者から見ると「パスワード再設定ができない」という結果になります。
ここが、トラブルシューティングで非常に重要なポイントです。「メールが届いた」という事実だけを見るのではなく、フローのどこまで成功しているのかを確認しなければいけません。
「メール送信OK」と「利用者が再設定できる」は別物
今回のケースでは、再設定申請、トークン発行、URL生成、SMTP送信、メール受信までは成功していました。ところが、URLクリック後の画面遷移で失敗していました。
そのため、バックエンドのログだけを見ていると、一見すると正常に見えてしまいます。サーバーからすると処理は終わっているからです。
しかし、利用者からすると何も終わっていません。この違いを理解しておくことが、今回の記事で最も重要です。
実際に起きた問題|再設定メールは届いたのにTOP画面が表示された
今回のトラブルは、最初からパスワード再設定リンクが問題だったわけではありません。その前に、メール送信そのものができないという問題がありました。
SMTP設定や認証情報を確認し、メールを送信できる状態まで修正しました。そして実際に「パスワードを忘れた方」からメールアドレスを入力すると、再設定メールが届くようになりました。ここまでは大きな前進でした。
SMTPを修正してメール送信までは成功した
再設定メールが届いたことで、少なくとも「再設定申請を受け付けている」「メール送信処理が呼び出されている」「SMTP接続ができている」「メール本文が生成されている」「宛先へメールが配送されている」といった部分は正常に動いていると判断できます。
「メールが届かない」という問題については解消しています。しかし、ここでテストを終えてはいけません。メールは、利用者を次の処理へ移動させるための入口だからです。
URLにも再設定トークンは含まれていた
届いたメールを見ると、パスワード再設定用のURLも存在していました。URLに再設定処理用のトークンが含まれている状態です。
これを見ると、「トークンも作られている」「URLもできている」「やはり完成しているのではないか」と思いやすくなります。
しかし、URLが存在していることと、URLをアプリ側が正しく処理できることは別です。
クリックするとTOP画面が表示された
実際にメール内のURLをクリックすると、パスワード変更画面ではなくTOP画面が表示されました。
この時点で分かったのは、メール生成の問題ではなく、リンクを開いた後の処理に問題があるということです。
| 処理 | 状態 |
|---|---|
| トークン発行 | 成功 |
| メール生成 | 成功 |
| SMTP送信 | 成功 |
| メール受信 | 成功 |
| URL生成 | 成功 |
| URLクリック | 可能 |
| パスワード変更画面 | 表示されない |
| 利用者による再設定 | 失敗 |
この整理をすると、確認範囲を大幅に絞れます。SMTPを何度も確認する必要はありません。疑うべき場所は、リンクを開いた後です。
原因はApp.tsx側の再設定トークン検出・画面遷移処理だった
今回の直接的な原因は、フロントエンド側にありました。App.tsxで、アクセスされたURLにパスワード再設定トークンが含まれているかを検出し、専用のパスワード変更画面へ切り替える処理が不足していたのです。
URLが正しくても画面は自動では切り替わらない
ここは非エンジニアがAIと一緒に開発していると、特に見落としやすいところです。
バックエンドで「このURLをクリックしてください」というURLを生成しただけでは、フロントエンドが自動的に意味を理解してくれるわけではありません。
たとえばURLに再設定用のパス、クエリパラメータ、トークンなどが含まれている場合、フロント側ではその情報を読み取り、「これは通常のTOP画面ではなく、パスワード再設定画面を表示するアクセスだ」と判断する処理が必要です。
フロントエンドがURLを読み取る必要がある
Reactなどで作ったシングルページアプリケーションでは、最初にアプリ本体が読み込まれ、その後にフロントエンド側で表示する画面を判断する構成もよくあります。
そのため、URLは正しいのに、App.tsxがそのURLを識別していないということが起こり得ます。
今回もその状態でした。URLそのものは存在していました。しかしアプリ側から見ると「通常アクセス」として処理されてしまい、TOP画面が表示されていました。
再設定トークンを検出して専用画面へ遷移させる
必要なのは、概念的には次の処理です。
URLへアクセス → 再設定用トークンがあるか確認 → ある場合はパスワード変更画面を表示 → 新パスワードを入力 → トークンと新パスワードをAPIへ送信
ここまでフロント側が担当します。
トークン検証 → 有効ならパスワード変更 → 使用済みトークンを無効化
こちらはバックエンド側の役割です。つまり、パスワード再設定は、バックエンドだけの機能でも、フロントエンドだけの機能でもありません。 両方がつながって初めて成立します。
パスワード再設定メールのリンクが動かないときの切り分け手順
問題が起きたときは、最初からコード全体を調べる必要はありません。利用者の操作順に確認すると、かなり効率よく原因を絞れます。
| 確認項目 | 正常な状態 | 異常なら疑う場所 |
|---|---|---|
| メールは届くか | 再設定メールを受信できる | SMTP・メール送信処理 |
| URLは本番ドメインか | 本番環境のURLになっている | 環境変数・URL生成処理 |
| URLにトークンがあるか | 再設定用情報が付いている | バックエンド |
| リンク先のアプリルーティングは実装済みか | パスワード変更画面が表示される | App.tsxなどフロント |
| トークン用途を確認しているか | password_reset用途のみ受け付ける | バックエンド |
| 有効期限を確認しているか | 期限切れを拒否できる | トークン検証 |
| passwordVersionを確認しているか | 旧状態のトークンを無効化できる | 認証設計 |
| jtiを確認しているか | 使用済みリンクを再利用できない | トークン管理 |
1.まずメールが届くか
届かないのであれば、最初にSMTPやメール送信処理を確認します。ここでフロントのルーティングを疑っても意味がありません。
2.次にメール内URLを見る
メールが届いたら、リンクをクリックする前にURLを確認します。開発環境やプレビュー環境のURLではなく、本番ドメインになっているかを見ます。
たとえば、「本番のメールなのにlocalhostを向いている」「以前のCloud Run URLになっている」「AI Studioの開発URLになっている」といった状態なら、フロント側の画面遷移以前にURL生成設定を直す必要があります。
3.実際にリンクをクリックする
URLを目視しただけでテストを終えないことも重要です。リンク先が正しく表示されるかは、実際にクリックしなければ分かりません。今回の問題も、クリックしたことで初めて発見できました。
4.TOP画面ならフロント側を疑う
メールが届き、URLも正しく、トークンも付いている。それでもTOP画面になる場合は、フロント側のルーティングやURL検出処理を優先して確認します。今回で言えばApp.tsxです。
5.画面が出ても安心しない
パスワード変更画面が表示されたら、今度はバックエンド側のトークン検証を確認します。「画面が表示された=安全に変更できる」とは限りません。用途、期限、使用済みかどうかなどを確認する必要があります。
メール内URLが本番URLにならないときは環境変数名を確認する
パスワード再設定メールでは、メール本文に本番URLを埋め込む必要があります。
このURLを環境変数から取得している場合、非常に単純なミスが原因になることがあります。それが、環境変数名の不一致です。
APP_URL・APP_UR・APP_BASE_URLのような名前違いに注意
たとえばコード側がAPP_URLを読みに行っているとします。ところがCloud Run側にはAPP_URという名前で登録されていたら、人間から見れば一文字違いですが、プログラムから見ればまったく別の名前です。
同じように、APP_URLとAPP_BASE_URLも別物です。
「本番URLはちゃんと登録した」ではなく、コード側が参照している環境変数名とCloud Run側の名前が完全一致しているかを確認する必要があります。
MAIL_REPLYTOとMAIL_REPLY_TOも同じ
メール関連の設定でも同様です。たとえば、MAIL_REPLYTOとMAIL_REPLY_TOでは名前が違います。
AIに設定を依頼していると、あるファイルではMAIL_REPLYTO、別のファイルではMAIL_REPLY_TOといった揺れが発生することがあります。
そのため、環境変数を確認するときは値だけではなく、変数名、コード側で参照している名前、本番環境に登録した名前の3つを照合することが重要です。
Cloud Runでは新しい設定を新しいリビジョンへ反映させる
Cloud Runでは、サービスの設定や環境変数はデプロイされるリビジョンに関連付けられます。環境変数を変更したつもりでも、現在トラフィックを受けているリビジョンにその変更が反映されているかまで確認する必要があります。
このため、環境変数を変更した後は、設定変更 → 新しいリビジョンへデプロイ → そのリビジョンが稼働していることを確認 → 本番で再テストという流れまで行います。
安全なパスワード再設定リンクを設計する
パスワード再設定機能では、「動けばよい」だけでは不十分です。認証に直接関わるため、安全設計も必要です。
OWASPのForgot Password Cheat Sheetでは、再設定トークンについて、十分安全に生成されること、適切な期間で失効すること、1回限りで使えることなどが推奨されています。また、存在するアカウントと存在しないアカウントで外部への応答内容を変えないことも推奨されています。
今回の実装では、これらの考え方を踏まえて確認します。
30分有効の署名付きトークンにする
今回の設計では、再設定トークンを30分有効としています。30分という時間そのものがすべてのシステムにおける絶対的な正解という意味ではありません。
重要なのは、無期限に使える再設定リンクを作らないということです。
利用者が再設定申請をしてから、合理的な時間内だけ有効にします。期限切れになったリンクからパスワードを変更しようとした場合は、再度パスワード再設定を申請してもらう設計にします。
purpose=password_resetを含める
トークンにpurpose=password_resetという用途情報を持たせる設計も有効です。
トークンが正しく署名されているかだけではなく、そのトークンが何のために発行されたものなのかを確認できます。
ログイン用、メール認証用、パスワード再設定用など複数のトークンを扱うシステムでは、用途を明確に分けることが重要です。パスワード再設定APIではpurpose=password_resetであることを確認し、別用途のトークンを受け付けないようにします。
passwordVersionを含める
さらに、トークンにpasswordVersionを持たせる考え方があります。
ユーザー情報に現在のパスワードバージョンを保持しておき、再設定トークン発行時点のバージョンをトークンにも含めます。
パスワード変更後にユーザー側のpasswordVersionを更新すれば、変更前に発行されたトークンを無効化できます。
再設定リンクAを発行
再設定リンクBを発行
Bでパスワードを変更
passwordVersionを更新
古いAを使おうとする
バージョン不一致で拒否
jtiで使用済みトークンを1回限りにする
jtiはトークンを識別するためのIDとして利用できます。再設定完了時に、そのjtiを使用済みとして記録します。
同じリンクが再び使われた場合は、「このトークンはすでに使用済み」として拒否します。
ここまで行うことで、「誰かが過去のメールを見つけ、以前の再設定URLを再利用する」といったリスクを減らせます。
登録済み・未登録メールで外部レスポンスを変えない
パスワード再設定画面で「そのメールアドレスは登録されていません」と明確に表示すると、第三者がアカウントの存在を確認できる可能性があります。
そのため、登録されている場合でも、登録されていない場合でも、「該当するアカウントが存在する場合は、パスワード再設定メールを送信しました」というように、外から見たレスポンスを揃える方法があります。
トークン全文やSMTPパスワードをログへ出さない
トラブル調査をしていると、「とりあえず全部ログへ出して確認したい」と思うことがあります。しかし、認証情報までログへ出すのは危険です。
少なくとも、SMTPパスワード、APIキー、パスワード、再設定トークン全文などの機密情報はログへそのまま残さないようにします。
再設定処理を調査する場合も、トークンの有無、検証成功・失敗、期限切れ、使用済み、jtiの一部または内部識別子など、問題特定に必要な範囲へ絞ります。
パスワード変更後に旧リンクと旧パスワードを無効化する
新しいパスワードを登録できたら、そこでテストを終えたくなります。しかし、もう少し確認が必要です。重要なのは、新しい認証情報だけが有効な状態になっているかです。
passwordVersionを更新する
パスワードを変更したら、ユーザーのpasswordVersionを更新します。これによって、以前のバージョンで発行された再設定トークンを無効化できます。
同じURLをもう一度クリックする
再設定完了後、先ほど使ったメール内URLをもう一度クリックします。
正常な設計なら、同じトークンを使ってもう一度パスワードを変更できない状態になっている必要があります。
ここを確認しなければ、「一回目の変更は成功したが、リンクが何度でも使える」という問題を見逃す可能性があります。
旧パスワードでもログインしてみる
次に、新しいパスワードでログインできることを確認します。さらに、変更前の古いパスワードでもログインしてみます。
期待する結果は、新パスワードではログイン成功、旧パスワードではログイン失敗です。
「新パスワードが使える」というテストだけでは、古い認証情報が確実に無効になったことまでは分かりません。
本番実機テストの完了条件チェックリスト
パスワード再設定機能では、次の7項目を本番環境で実際に確認することをおすすめします。
パスワード再設定機能の完了条件
☐ 再設定メールが届く
☐ メール内URLが本番ドメインになっている
☐ リンクをクリックすると新パスワード設定画面が表示される
☐ 再設定後、新しいパスワードでログインできる
☐ 古いパスワードではログインできない
☐ 同じ再設定リンクを再利用すると拒否される
☐ 再デプロイ後も新しいパスワードでログインできる
この7項目の中で、今回最初に抜けていたのが3番目です。メールが届いたため、1番は成功していました。URLも生成されていました。しかしリンクをクリックした先で、再設定画面が出ませんでした。
もし「メールが届いたから完成」という判定をしていたら、そのまま利用者へ提供していた可能性があります。
再デプロイ後も確認する理由
最後の「再デプロイ後も新しいパスワードでログインできる」という確認も重要です。
Webアプリでは、コードを修正して再デプロイすることがあります。そのとき、パスワードや利用者データをコンテナ内部の一時的な保存領域だけに持っていると、別の問題が発生する可能性があります。
認証情報を永続化する必要があるシステムでは、再デプロイによって状態が失われない設計になっているかも確認します。
AIの「実装しました」を完成条件にしてはいけない

AIを使った開発では、非常に便利な一方で、独特の落とし穴があります。それが、AIの説明文を完成判定にしてしまうことです。
AIに「パスワード再設定機能を実装してください」と依頼すると、「実装しました」「再設定トークンを追加しました」「メールから変更できるようになりました」と説明してくれることがあります。
しかし、その文章は、本番環境で利用者が最後まで操作できた証拠ではありません。
AIが確認できるのはコード上の状態までの場合がある
AIがコードを変更できたとしても、GitHubへ正しく反映されたか、Cloud Buildがそのコードをビルドしたか、Cloud Runに新しいRevisionが作られたか、最新Revisionへトラフィックが向いているか、本番ドメインからアクセスできるか、実際のメール本文が正しいか、利用者が最後まで操作できるかは別の確認です。
1.コード修正を確認する
まずAIが意図したコードを本当に変更しているか確認します。今回であれば、App.tsxに再設定URLを判定する処理が追加されているかを見ます。
2.GitHubのSHAを確認する
次に、GitHubへ変更が反映されているか確認します。コミットにはSHAと呼ばれる識別情報があります。AIや開発環境で見ているコードと、GitHub上のコードが同じ時点のものかを確認する材料になります。
3.Cloud Buildを確認する
GitHubへ反映されても、そのコードが本番へデプロイされているとは限りません。Cloud Buildを使っている場合は、正しいコミットからビルドされたか、ビルドが成功しているかを確認します。
4.Cloud RunのRevisionを確認する
Cloud Buildが成功したら、Cloud Run側を確認します。最新Revisionが作られているか、意図したコンテナイメージか、必要な環境変数があるか、本番トラフィックが正しいRevisionへ向いているかを確認します。
5.最後は人間が本番URLを操作する
そして最後が最も重要です。本番URLを開き、利用者と同じ操作を人間が行います。
パスワード再設定を申請 → メールを受信 → リンクをクリック → 新パスワード画面を確認 → 新パスワードを登録 → 新PWでログイン → 旧PWを拒否 → 同じリンクを再利用して拒否
完成判定を一本の流れにすると、AIによる修正 → コード → GitHub SHA → Cloud Build → Cloud Run Revision → 本番URL → 人間による実機テストとなります。
「できました」と「本番で動く」は別物です。
この考え方は、パスワード再設定だけではありません。メール送信、PDF生成、決済、管理画面、データ保存など、AIを使って開発するさまざまな機能に共通します。
パスワード再設定でよくある失敗
メールが届いた時点でテストを終了する
再設定メールは目的ではなく、途中の手段です。リンクをクリックし、新しいパスワードでログインするところまで確認します。
URLを目視しただけでクリックしない
URLが正しく見えても、アプリ側がそのURLを処理できるとは限りません。今回のように「URLは正しいがTOP画面になる」ということもあります。実際にクリックします。
バックエンドだけ修正してフロントを確認しない
パスワード再設定はバックエンドとフロントエンドをまたぐ機能です。トークンを発行するだけでは利用者は操作できません。フロント側にURL検出、再設定画面表示、新パスワード入力、API送信まで必要です。
環境変数の値だけ見て変数名を見ない
「値は正しいのに動かない」というときは、変数名を確認します。APP_URLとAPP_UR、MAIL_REPLYTOとMAIL_REPLY_TOは別の変数です。一文字違うだけでもプログラムには伝わりません。
Cloud Build成功=機能成功だと思う
Cloud Buildの成功は「ビルド処理が成功した」という意味です。利用者がその機能を使えたことを意味するわけではありません。Cloud Run、URL、フロント、実操作まで続けて確認します。
新パスワードだけ試して旧パスワードを確認しない
新しいパスワードで入れたら一見成功に見えます。しかし旧パスワードが残っていないかも確認します。認証機能では、通るべきものが通るだけではなく、拒否されるべきものが拒否されるところまでテストすることが重要です。
パスワード再設定機能の完成条件を最初にAIへ伝える
今回の経験は、AIへの依頼方法にも活かせます。単に「パスワード再設定機能を実装してください」と依頼するより、完成条件を具体的に書いたほうが確認しやすくなります。
たとえば、次のように考えます。
ゴール
利用者がパスワードを忘れた場合、自分のメールアドレスから新しいパスワードへ変更できる。
完成条件
- 再設定メールが本番環境から届く
- メール内URLは本番ドメイン
- URLを開くと新パスワード入力画面になる
- トークンの期限を検証する
- パスワード変更後は旧PWを拒否する
- 同じ再設定URLは再利用できない
- 再デプロイ後も変更後パスワードを利用できる
ここまで定義しておくと、「メールを送れるようにしたので完成」という途中終了を減らせます。AIへ依頼するときにも、実装内容ではなく、利用者が最後にどうなれば完成なのかを伝えることが重要です。
まとめ|完成条件は「利用者が最後まで操作できること」
今回のパスワード再設定トラブルでは、最初にSMTPの問題を修正し、再設定メール自体は届くようになりました。メール内にはURLも存在していました。しかしクリックすると、新しいパスワードを入力する画面ではなくTOP画面が表示されました。
原因は、フロント側のApp.tsxで再設定トークンを検出し、パスワード変更画面へ遷移する処理が不足していたことでした。
この経験から得られる最大のポイントは、バックエンドだけ直しても機能は完成しないということです。
さらにAI開発では、AIが「実装しました」と言ったことを完成条件にしないことも重要です。
メールが届く → URLが本番ドメイン → 再設定画面が開く → 新しいパスワードに変更できる → 新PWでログインできる → 旧PWは拒否される → 使用済みURLは拒否される → 再デプロイ後もログインできる
セキュリティ面では、期限付き・1回限りのトークンや、既存・非既存アカウントで外部レスポンスを揃える設計も重要です。
そしてAI開発全体では、AIによる修正 → GitHub → Cloud Build → Cloud Run → 本番環境 → 人間による実機テストまでを一つの確認工程として考えます。
コードが存在することが完成ではありません。ビルドが通ったことも完成ではありません。メールが届いたことも完成ではありません。
利用者が本番環境で、期待した操作を最後まで完了できること。
それを完成条件にすると、AIを使ったアプリ開発でも「直したはずなのに動かない」という状態を減らしやすくなります。
FAQ
パスワード再設定メールは届くのにTOP画面になる原因は何ですか?
メール内URLを開いた際に、フロントエンドが再設定用URLやトークンを正しく検出できていない可能性があります。今回の実例では、App.tsx側に再設定トークンを検出し、専用のパスワード変更画面へ遷移する処理が不足していました。
メール内URLにトークンが付いていればバックエンドは正常ですか?
少なくともトークン発行やURL生成まで進んでいる可能性は高まりますが、それだけで再設定機能全体が正常とは判断できません。フロント側の画面遷移、トークン検証、パスワード更新、新しいパスワードでのログインまで確認してください。
Cloud Runの環境変数を変更したらすぐ本番へ反映されますか?
環境変数を含む設定が、現在トラフィックを受けているリビジョンに反映されているかを確認する必要があります。設定変更後は、新しいリビジョンへの反映と本番での再テストまで行ってください。
パスワード再設定リンクは一度使ったら無効にすべきですか?
安全性を高めるため、再設定用トークンは一回限りで利用でき、一定期間で失効する設計が推奨されます。
AIが「実装しました」と回答したら、どこまで確認すれば完成ですか?
コード修正だけでなく、GitHubへの反映、Cloud Build、Cloud RunのRevision、本番URLを順に確認し、最後は利用者と同じ操作を人間が実施してください。パスワード再設定なら、新PWでのログイン、旧PWの拒否、使用済みリンクの再利用拒否まで確認して完成と判断します。
CTA
AIでアプリを修正していても、「コードは直ったはずなのに本番では動かない」「メールやビルドまでは成功しているのに、どこで止まっているのか分からない」という状態になることがあります。
重要なのは、コードだけではなく、利用者が操作する一連の流れを確認することです。
自社だけで原因の切り分けが難しい場合や、AI・GitHub・Cloud Build・Cloud Runを含む開発の進め方を整理したい場合は、現在起きている症状や開発環境を整理したうえでご相談ください。
参考情報
技術的な安全設計・Cloud Runの設定確認は、実装時点の公式ドキュメントもあわせて確認してください。
