はじめに — ログイン後に「どこに戻るか」問題
OAuth 認証を実装したことがある方なら、一度はこの問題に出くわしたことがあるかもしれません。
「ユーザーがログインした後、元いたページに戻したい。でもリダイレクト先の情報が消えてしまう」
単純に聞こえるこの課題が、実際にはかなり厄介でした。Wakulier(フリーランス向け依頼管理ツール)の開発初日に、この問題で数時間を費やすことになりました。
背景 — Wakulier の認証設計
Wakulier は Next.js 16 + Supabase で構築しているプロダクトです。認証には Supabase Auth の Google OAuth を採用しています。
フリーランスのユーザーが多いため、Google アカウントでのログインは自然な選択でした。ただし、OAuth のフローには「外部プロバイダへのリダイレクト → コールバック → アプリに戻る」という複数ステップがあります。この間に「元いたページ」の情報をどう保持するかが問題になります。
2025年12月27日の開発日記には、こう書いています:
ログイン後のリダイレクト先が消える問題。
- 試行1: Cookie → Supabaseのcallback処理で消える
- 試行2: Query parameter → 同上
- 試行3: localStorage → 成功(クライアント側で保持)
この3行の裏には、それぞれ異なるアプローチを試しては失敗した試行錯誤がありました。
方法1: Cookie で保持する(失敗)
最初に試みたのは、最も一般的に思えるアプローチでした。ログインボタンを押した時点で、現在のURLを Cookie に保存し、コールバック後に読み取る方法です。
// ログイン前にリダイレクト先を Cookie に保存
document.cookie = `redirect_to=${encodeURIComponent(currentPath)}; path=/`;
// コールバック後に Cookie から読み取り
const redirectTo = getCookie("redirect_to");
なぜ失敗したか
Supabase Auth の OAuth フローでは、認証プロバイダ(Google)からコールバックURLに戻ってきた後、Supabase の内部処理でセッションを確立します。この過程でページ遷移が複数回発生し、Cookie の状態が期待通りに維持されませんでした。
具体的には、Supabase の auth/callback ルートでの処理中に、Cookie が上書きされたり、パスの不一致でアクセスできなくなるケースがありました。SameSite 属性や Secure フラグの設定で回避できる可能性はありましたが、Supabase の内部的なリダイレクトチェーンを完全にコントロールすることは難しいと判断しました。
方法2: Query Parameter で保持する(失敗)
次に試したのは、OAuth の認証URLに Query Parameter としてリダイレクト先を含める方法です。
// ログイン時に redirectTo をパラメータとして渡す
await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: `${origin}/auth/callback?redirect_to=/dashboard`,
},
});
なぜ失敗したか
このアプローチも同じ理由で失敗しました。Supabase Auth がコールバック処理を行う際、Query Parameter が内部のリダイレクトで失われてしまいます。
Supabase の signInWithOAuth が生成するURLは、まず Google の認証画面にリダイレクトし、承認後に Supabase のサーバーを経由してからアプリのコールバックURLに戻ってきます。この「Supabase のサーバー経由」の部分で、カスタムの Query Parameter が落ちてしまうのです。
開発日記の「同上」という一言が、この時の落胆を物語っています。Cookie と同じ根本原因 — OAuth フローの途中で外部サービスを経由するため、リクエストに紐づいた状態が消失する — に気づいたのはこの時でした。
発想の転換 — サーバーサイドにこだわるべきか
Cookie と Query Parameter が同じ理由で失敗したことで、「HTTP リクエストに状態を載せる」アプローチ自体に限界があると感じ始めました。ここで Claude Code に相談しました。
Zeronova
Cookie も Query Parameter も Supabase のコールバック処理で消えてしまう。そもそも「どこに戻るか」の情報を OAuth フローに載せるのが間違いなのかもしれない。Claude Codeその直感は正しいと思います。リダイレクト先の記憶は認証そのものとは無関係な情報です。OAuth フローの外で管理する方が確実です。localStorage なら、リダイレクトチェーンの影響を受けません。Zeronova
localStorage か。SSR でアクセスできない点が気になるけど、リダイレクト先の読み取りはコールバック後のクライアント側の処理だから問題ないか。Claude Codeはい。書き込みはログインボタンのクリック時(Client Component)、読み取りはコールバックページのマウント時(useEffect)なので、どちらもクライアント側で完結します。ただし、オープンリダイレクト対策として、保存する値は内部パスに限定してください。
この対話で「認証フローと状態保持を分離する」という方針が固まりました。結果的に、これが正解でした。
方法3: localStorage で保持する(成功)
3回目の挑戦で、発想を変えました。サーバーサイドやURL経由で状態を渡そうとするのではなく、ブラウザのクライアントサイドに保持する方法です。
// ログイン前に localStorage に保存
localStorage.setItem("auth_redirect_to", currentPath);
// コールバック後に localStorage から読み取り
const redirectTo = localStorage.getItem("auth_redirect_to");
if (redirectTo) {
localStorage.removeItem("auth_redirect_to");
router.push(redirectTo);
}
なぜ成功したか
localStorage はブラウザのクライアントサイドストレージです。OAuth のリダイレクトチェーンがどれだけ複雑でも、同一オリジンであれば localStorage の値は保持されます。
ポイントは以下の3つです:
- OAuth フローに依存しない: サーバー経由のリダイレクトに左右されない
- 同一オリジンで確実にアクセス可能: 認証後にアプリに戻ってきた時点で読み取れる
- 使い捨てが容易: 読み取り後に
removeItemするだけ
開発日記にも「成功(クライアント側で保持)」とあるように、サーバーサイドの状態管理から離れたことで問題が解決しました。
3つの方法の比較
| 方法 | 動作 | OAuth フロー耐性 | 注意点 |
|---|---|---|---|
| Cookie | リクエストに自動付与 | 低(外部リダイレクトで失われやすい) | SameSite/Secure 設定が複雑 |
| Query Parameter | URLに状態を含める | 低(中間サーバー経由で失われる) | URLが長くなる、セキュリティリスク |
| localStorage | ブラウザに保持 | 高(クライアントサイドで完結) | SSRでアクセス不可 |
localStorage 方式の注意点
localStorage が万能というわけではありません。実装時に考慮すべき点がいくつかあります。
SSR との相性
Next.js の Server Component からは localStorage にアクセスできません。リダイレクト先の読み取り・書き込みは Client Component で行う必要があります。
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
export function AuthCallbackHandler() {
const router = useRouter();
useEffect(() => {
const redirectTo = localStorage.getItem("auth_redirect_to");
if (redirectTo) {
localStorage.removeItem("auth_redirect_to");
router.push(redirectTo);
} else {
router.push("/dashboard");
}
}, [router]);
return <div>認証処理中...</div>;
}
セキュリティ上の考慮
localStorage に保存する値はリダイレクト先のパスだけに限定すべきです。外部URLや任意の値を保存してリダイレクトに使うと、オープンリダイレクトの脆弱性になります。
// ✅ 安全: 内部パスのみ許可
const safePath = redirectTo?.startsWith("/") ? redirectTo : "/dashboard";
router.push(safePath);
複数タブでの挙動
localStorage は同一オリジンの全タブで共有されます。複数タブで同時にログインフローを開始した場合、最後に書き込んだ値で上書きされます。実用上は問題になりにくいですが、sessionStorage を使えばタブごとに分離できます。
なぜ最初から localStorage を試さなかったのか
振り返ると、「認証状態はサーバーサイドで管理すべき」という先入観がありました。
Cookie や Query Parameter はHTTPリクエストに紐づいた状態管理の定番です。OAuth も HTTP ベースのプロトコルなので、HTTP の仕組みで解決するのが筋だと考えていました。
しかし、Supabase Auth のように外部サービスがリダイレクトを仲介する場合、HTTP リクエスト間で状態を渡すこと自体が難しくなります。「どこに戻るか」という情報は認証そのものとは無関係なので、認証フローの外で管理する方が合理的でした。
OAuthフローでは状態保持が難しい。localStorageが無難
開発日記のこの結論は、3回の試行錯誤を経て得た実感でした。
他の認証ライブラリでの状況
Supabase Auth 固有の問題かと思われるかもしれませんが、同様の課題は他の認証ライブラリでも起こり得ます。
- NextAuth.js(Auth.js):
callbackUrlパラメータで対応可能だが、カスタマイズが必要なケースもある - Firebase Auth:
signInWithRedirect後のgetRedirectResultで処理するが、状態の受け渡しには同様の工夫が必要 - Clerk: セッション管理が統合されているため比較的スムーズ
ポイントは、どの認証ライブラリを使っていても「外部プロバイダ経由のリダイレクト」がある限り、状態保持の問題は共通して発生するということです。localStorage 方式は、認証ライブラリに依存しない汎用的な解決策として機能します。
関連記事
この記事で扱った OAuth の課題は、他の Journal 記事でも関連するトピックを扱っています:
- LINE 内ブラウザで OAuth 認証が動かない問題への対処 — OAuth のブラウザ互換性問題
- Next.js App Router で Supabase 認証を使う — middleware.ts がカギ — Supabase Auth の全体設計
- 個人開発でもやるべきセキュリティ監査チェックリスト — 認証まわりのセキュリティ全般
まとめ
OAuth リダイレクトで状態を保持する方法を3つ試した結果、localStorage が最もシンプルで確実な解決策でした。
持ち帰りポイント:
- Cookie・Query Parameter は OAuth の外部リダイレクトで失われやすい — 特に Supabase Auth のように中間サーバーを経由する場合
- localStorage はクライアントサイドで完結するため、リダイレクトチェーンに影響されない — ただし SSR との組み合わせには注意
- 「認証フロー」と「リダイレクト先の記憶」は分離して考える — 認証とは無関係な情報を認証フローに載せようとすると複雑になる
Wakulier では、Claude Code と協力してこの認証フローを1日で構築しました。3回の失敗があったからこそ、最終的にシンプルな解決策にたどり着けたと感じています。「正攻法がうまくいかない時は、問題の切り分け方を変えてみる」という教訓は、OAuth に限らず役立つ考え方だと思います。
Zeronova(ゼロノバ)
Product Manager / AI-Native Builder
Web/IT業界19年以上・20以上のWebサービスを担当したPdM。東証プライム上場企業の子会社代表として事業経営を経験。現在はAIを駆使して企画から実装まで完結させる個人開発を実践中。