はじめに
招待リンク(例: /invite/abc)をクリックしたユーザーが未ログインだった場合、一度ログインページへ遷移させ、ログイン完了後に元の招待ページへ戻したいという要件はよくあります。
この記事では、redirect クエリパラメータを使用してユーザーの元の場所を復元する方法と、その際に必ず実装すべきセキュリティ対策について解説します。
この記事でわかる・できること
- ログイン後に元のページ(招待画面など)にユーザーを自動で戻す実装方法
- 「オープンリダイレクト脆弱性」を防ぐためのセキュリティ対策
- Firebase Authの
signInWithPopupとsignInWithRedirectそれぞれに適したパラメータ保持の方法
この記事の対象者
- Next.js (App Router) で認証機能を実装している方
- ログイン後の画面遷移フローを改善したい方
- リダイレクト処理のセキュリティリスクについて理解したい方
動作環境・使用するツールや言語
- Next.js: 13.0以上 (App Router使用)
- Firebase Auth: v9以上
- React: 18以上
1. 基本的な実装フロー
招待ページから未ログイン状態でアクセスがあった場合、通常はミドルウェアやガード処理で以下のようにリダイレクトさせます。
https://example.com/login?redirect=/invite/abc
ログインページでは、この redirect パラメータを取得し、認証成功後にそのパスへ遷移させます。
実装コード例
以下は、redirect クエリパラメータを読み取り、ログイン後に元の場所へ戻す基本的な実装です。
"use client";
import { useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { onAuthStateChanged, signInWithPopup, GoogleAuthProvider } from "firebase/auth";
import { auth } from "@/app/lib/firebase";
export default function LoginPage() {
const router = useRouter();
const searchParams = useSearchParams();
// URLクエリまたはsessionStorageからリダイレクト先を取得
const redirectPath = searchParams.get("redirect") || sessionStorage.getItem("postLoginRedirect") || "/menus";
useEffect(() => {
// 【セキュリティ対策】アプリ内パスのみ許可する検証関数
const safeRedirect = (path) => {
// 1. パスが存在する
// 2. "/" で始まっている(相対パス)
// 3. "//" で始まっていない(プロトコル指定の絶対URL回避)
if (path && path.startsWith("/") && !path.startsWith("//")) {
return path;
}
return "/menus"; // デフォルトの戻り先
};
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
// ログイン済みなら安全なパスへリダイレクト
router.replace(safeRedirect(redirectPath));
// 使用済みのsessionStorageはクリアしておく
sessionStorage.removeItem("postLoginRedirect");
}
});
return () => unsubscribe();
}, [router, redirectPath]);
// ... (ログインボタン等のUI実装)
}
2. 【重要】オープンリダイレクト脆弱性への対策
リダイレクト機能を実装する際、「オープンリダイレクト(Open Redirect)脆弱性」 への対策は必須です。
なぜ危険なのか
対策をしない場合、攻撃者が作成した以下のようなURLをユーザーが踏んでしまう可能性があります。
https://yourapp.com/login?redirect=https://evil-site.com/fake-login
ユーザーは信頼できるドメイン(yourapp.com)でログインしたつもりが、ログイン直後に悪意のあるサイト(evil-site.com)へ転送され、情報を盗まれるフィッシング詐欺の踏み台にされてしまいます。
オープンリダイレクト脆弱性の仕組みと対策
対策方法:絶対URLを拒否する
リダイレクト先は必ずアプリ内のパスに限定する必要があります。
const safeRedirect = (path) => {
// 外部サイト(http~, //~)への遷移をブロックし、アプリ内ルートのみ許可する
return (path && path.startsWith("/") && !path.startsWith("//"))
? path
: "/menus";
};
このように検証を通すことで、redirect=https://... のような外部URLが渡されても、強制的にデフォルトページ(/menus)へ遷移させることができます。
3. 認証方式によるパラメータ保持の違い
Firebase Authなどを使用する場合、認証方式によってリダイレクトパラメータの扱いが変わります。
パターンA:signInWithPopup(ポップアップ方式)
PCブラウザなどでよく使われる方式です。
- 特徴: 同一ページ上でポップアップウィンドウが開く
-
メリット: ページ遷移が発生しないため、URLの
?redirect=/invite/abcがそのまま残る -
実装: 特別な保存処理は不要。
useSearchParamsから直接読み取ればOK
パターンB:signInWithRedirect(リダイレクト方式)
モバイルブラウザなどで推奨される方式です。
- 特徴: Google等の認証ページへ画面全体が遷移し、認証後に戻ってくる
-
問題点: 外部サイトへ行って戻ってくる間に、URLのクエリパラメータ(
?redirect=...)が消失する場合あり -
対策:
sessionStorageへの一時保存が必要
まとめ
招待リンクなどのフローを維持するためには、以下の3点を守って実装しましょう。
-
基本フロー: 未ログイン時は
?redirect=...を付与してログインページへ誘導する - セキュリティ: 戻り先は必ず検証し、外部サイトへのリダイレクト(オープンリダイレクト)を防ぐ
-
認証方式:
signInWithRedirectを使う場合は、パラメータが消えないようsessionStorageを活用する