ログインはちゃんとできるのに、ページをリロードするたびに(または少し経つと)ログイン画面に戻される。これは vibe コーディングで作ったアプリで最も多い認証バグです。原因はほぼ必ず次の5つのどれかなので、自分に当てはまるものへ飛んでください。

原因を素早く特定する

  • A. リロードのたびに追い出される → アプリが、認証ライブラリがセッションを復元する*前に*「ログインしているか?」を確認している(最多)
  • B. 30〜60分ほどで切れる → トークンの自動更新がオフになっている
  • C. そもそも一度も保存されない(シークレット / cookie ブロック) → セッションの保存先(localStorage・cookies)がブロックされている
  • D. デプロイした後だけ壊れた(Next.js など) → サーバー側のミドルウェアが不足している
  • E. フロントとバックエンドが別ドメイン → cookie の SameSite/Secure 設定ミス

A. リロードで追い出される — セッションの復元を待っていない(最多)

Supabase も Firebase も、保存されたログインをストレージから読み込むのにほんの一瞬(非同期)かかります。AI が生成したコードが、画面が表示された瞬間に if (no user) redirect to login を実行すると、その一瞬の「まだ読み込み中」の状態をログアウト状態と勘違いして、毎回追い出してしまいます。

ポイント:「まだ確認中」の状態を別で持ち、認証が確定するまでローディングを表示すること。

Firebase(v9 modular):

js

import { getAuth, onAuthStateChanged } from "firebase/auth";

const auth = getAuth();
onAuthStateChanged(auth, (user) => {
  if (user) {
    // logged in — render the app
  } else {
    // only redirect to login when truly signed out
  }
});

onAuthStateChanged は SDK が保存済みセッションを復元した*後に*発火します。判定はこのコールバックの中で行ってください。マウント時に auth.currentUser を読まないこと(その瞬間はまだ null のことがあります)。

Supabase:

js

// once on load: read the saved session
const { data: { session } } = await supabase.auth.getSession();

// then subscribe to changes
supabase.auth.onAuthStateChange((event, session) => {
  // session present = logged in
});

React では loading フラグを追加し、最初のコールバックが来るまで true のままにして、loading の間は絶対にリダイレクトせず、スピナーだけを表示します。

AI へのプロンプト:

> 「認証の確認中は loading 状態を出して、最初の onAuthStateChanged / onAuthStateChange コールバックが実行されるまではログインページにリダイレクトしないで。」

B. しばらくすると切れる — トークンの自動更新を確認

アクセストークンはたいてい1時間ほどで期限切れになります。自動更新されないと、「何もしていないのにログアウトされる」状態になります。

Supabase クライアントを作るとき、これらがオンになっているか確認してください(ブラウザではデフォルトでオンです。無効にしていたら戻してください):

js

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
  auth: {
    persistSession: true,     // save session to localStorage
    autoRefreshToken: true,   // refresh token before it expires
    detectSessionInUrl: true, // detect the session in the URL after OAuth
  },
})

persistSession はセッションを localStorage に保存し、autoRefreshToken は期限切れ前にトークンを更新します。よくあるミスは、カスタムの auth 設定を渡した際に、これらをうっかり false にしてしまうことです。

Firebase はデフォルトで local 永続化なので、ブラウザを閉じても保持され、トークンも自動で更新されます。

C. そもそも保存されない — シークレット / cookie ブロック

セッションは localStorage(または cookies)に保存されます。その保存先がブロックされていると、ログイン直後でも切れてしまいます。

  • シークレット / プライベートモードcookie のブロックサードパーティ cookie・データのブロックをチェックしてください。Firebase の local 永続化は「ブラウザがこのストレージ機構をサポートしている場合(例:サードパーティ cookie・データが有効になっている場合)」のみ動作します。
  • Safari のトラッキング防止(ITP)はクロスドメインのストレージを制限することがあります。
  • Firebase の永続化を明示的に固定するには、サインインの前に設定します:
js

import { getAuth, setPersistence, browserLocalPersistence } from "firebase/auth";

const auth = getAuth();
await setPersistence(auth, browserLocalPersistence); // then call signIn

browserSessionPersistence(タブを閉じるとクリア)や inMemoryPersistence(リロードでクリア)に設定されていると、ログアウトされるのは当然です。browserLocalPersistence に切り替えてください。

D. デプロイ後にログアウトされる(Next.js)— サーバーミドルウェア不足

Next.js(App Router)でサーバーレンダリングをしながら Supabase を使う場合、セッションは localStorage ではなく cookies に置く必要があります。しかし Server Components は cookie を書き込めないため、ミドルウェアがトークンを更新しない限り、期限切れのトークンが残り、リロードでログアウトされます。

対処:

  1. @supabase/ssr を使う:ブラウザでは createBrowserClient、サーバーでは createServerClient
  2. プロジェクトルートに middleware.ts を追加し、リクエストごとにセッションを更新する(公式の updateSession パターン)。
  3. サーバー側では、ページを保護するのに getSession() を信用しないこと。getClaims()(または getUser())を使う。getSession() はトークンの再検証が保証されていません。

ミドルウェアは Supabase 公式の SSR ドキュメント(下の sources 参照)からそのままコピーするのが最も安全です。

AI へのプロンプト:

> 「@supabase/ssr の createBrowserClient/createServerClient に分けて、リクエストごとに updateSession を実行する middleware.ts を追加し、サーバールートは getUser で保護して。」

E. ドメインが異なる — cookie の SameSite/Secure

自前のバックエンド(Express など)がログインを cookie に保存していて、フロントエンドと API が別ドメインにある場合、cookie が保存・送信されずログアウトされます。

  • 同一ドメインSameSite=Lax(未指定時のデフォルト)。たいていこれで解決します。
  • 別ドメイン(クロスサイト)SameSite=None; Secure を必ずセットで送る必要があります。ブラウザは Secure(HTTPS)なしの SameSite=None を拒否します。
  • そのため、http の localhost でテストするSecure cookie が落ちて、状態が保持されないことがあります。ローカルでは同一オリジンにプロキシするか、https で配信してください。

cookie の例:

text

Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax; Path=/

(ドメインをまたぐ場合は SameSite=None; Secure を使います。)

SameSite=Strict は、別のサイトから来たとき(例:OAuth プロバイダーから戻ってきたとき)に cookie を落とし、ログイン画面に飛ばすことがあります。ほとんどの場合は Lax が正解です。

それでも解決しない — 原因を特定できないとき

ここには万能な一行の解決策はありません。原因はスタックによって変わります。次の順で絞り込んでください:

  1. DevTools → Application → Local Storage / Cookies を開き、ログイン後にセッションの値が実際に保存されているか確認する。保存されていない → C(保存先がブロック)または E(cookie 設定)。
  2. 保存されているのにリロードで消える → A(復元のタイミング)または D(SSR ミドルウェア)。
  3. しばらく経ってから切れるだけ → B(トークン更新)。
  4. どれも効かない場合は、下の sources にある公式のサンプルコードを貼り付けて、まず最小限の再現を動かしてみてください。