OAuth リダイレクトで状態を保持する方法の比較 — Cookie・Query Parameter・localStorage

2026.02.10
Share:

はじめに — ログイン後に「どこに戻るか」問題

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行の裏には、それぞれ異なるアプローチを試しては失敗した試行錯誤がありました。

最初に試みたのは、最も一般的に思えるアプローチでした。ログインボタンを押した時点で、現在の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 に相談しました。

ZeronovaZeronova
Cookie も Query Parameter も Supabase のコールバック処理で消えてしまう。そもそも「どこに戻るか」の情報を OAuth フローに載せるのが間違いなのかもしれない。
Claude Code
その直感は正しいと思います。リダイレクト先の記憶は認証そのものとは無関係な情報です。OAuth フローの外で管理する方が確実です。localStorage なら、リダイレクトチェーンの影響を受けません。
ZeronovaZeronova
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つです:

  1. OAuth フローに依存しない: サーバー経由のリダイレクトに左右されない
  2. 同一オリジンで確実にアクセス可能: 認証後にアプリに戻ってきた時点で読み取れる
  3. 使い捨てが容易: 読み取り後に removeItem するだけ

開発日記にも「成功(クライアント側で保持)」とあるように、サーバーサイドの状態管理から離れたことで問題が解決しました。

3つの方法の比較

方法動作OAuth フロー耐性注意点
Cookieリクエストに自動付与低(外部リダイレクトで失われやすい)SameSite/Secure 設定が複雑
Query ParameterURLに状態を含める低(中間サーバー経由で失われる)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 記事でも関連するトピックを扱っています:

まとめ

OAuth リダイレクトで状態を保持する方法を3つ試した結果、localStorage が最もシンプルで確実な解決策でした。

持ち帰りポイント:

  1. Cookie・Query Parameter は OAuth の外部リダイレクトで失われやすい — 特に Supabase Auth のように中間サーバーを経由する場合
  2. localStorage はクライアントサイドで完結するため、リダイレクトチェーンに影響されない — ただし SSR との組み合わせには注意
  3. 「認証フロー」と「リダイレクト先の記憶」は分離して考える — 認証とは無関係な情報を認証フローに載せようとすると複雑になる

Wakulier では、Claude Code と協力してこの認証フローを1日で構築しました。3回の失敗があったからこそ、最終的にシンプルな解決策にたどり着けたと感じています。「正攻法がうまくいかない時は、問題の切り分け方を変えてみる」という教訓は、OAuth に限らず役立つ考え方だと思います。

Zeronova avatar

Zeronovaゼロノバ

Product Manager / AI-Native Builder

Web/IT業界19年以上・20以上のWebサービスを担当したPdM。東証プライム上場企業の子会社代表として事業経営を経験。現在はAIを駆使して企画から実装まで完結させる個人開発を実践中。

関連プロダクト

Wakulier(ワクリア)

継続案件の依頼管理ツール