エラー内容
Unable to process request due to missing initial state. This may happen if browser sessionStorage is inaccessible or accidentally cleared. Some specific scenarios are - 1) Using IDP-Initiated SAML SSO. 2) Using signInWithRedirect in a storage-partitioned browser environment.
概要
Firebase Authentication を用いてsignInWithRedirect()
でGoogleアカウントでのログインを実装したところ発生。
なぜ起きるのか。
FirebaseのsignInWithRedirect()
は次のように動く。
- ログインボタン押下 → Firebaseが一時的な状態を
sessionStorage
に保存する - Googleのログイン画面(認証ページ)にリダイレクト
- ログイン成功後 → 元のアプリ(http://localhost:3000/)にリダイレクト
- Firebaseが
sessionStorage
からデータを読み取って「このリダイレクトはログイン処理の続きだ」と判断し、セッションを復元。getRedirectResult()
で最終的にユーザ情報を取得。
つまり「この途中のsessionStorage」が消えるとFirebaseは「前の状態を再現できない!」となって上記エラーを出します。
途中状態の保持にブラウザのsessionStorage
を使っているのが重要です。
signInWithRedirect()
はsessionStorage
に一時データを書き込んでからGoogleに遷移しますが、戻ってきた時にsessionStorage
が空だとまさにこのエラーになります。
これはFirebase側の不具合ではなく、ブラウザ仕様+リダイレクト認証の副作用です。
ブラウザはFirefoxを使用しています。
ChatGPTに上記の副作用の点を質問すると、
最近のブラウザ(特にFirefoxとSafari)では、プライバシー強化のため、サイト分離(Storage Partitioning)という仕様が導入されています。
これにより
- サードパーティコンテキスト(=Googleログイン画面のような別ドメイン)から戻ってきた時、
- 元のサイトの
sessionStorage
が初期化される(=空になる)場合があります。
その結果Firebaseが状態を復元できず、最初に書いたエラーが発生する
とのことです。
このエラーが出る代表的な状況
原因 | 説明 |
---|---|
Next.js(App Router) × サーバサイドで動くコード |
signInWithRedirect() はクライエント側でしか動きません。"use client" がついていないと失敗 |
ブラウザの設定(サードパーティCookie・partitioned storage) | SafariやBraveのように、ストレージを分離しているブラウザで起こることがあります |
Next.jsのHot Reload(開発中) | 開発サーバーの再起動・リロードでsessionStorage が消えてエラーになることも |
複数ドメイン(localhost → firebaseapp.com)間のリダイレクト | ドメインが違うとsessionStorageが共有されず、初期状態を復元できない |
解決策
参考(公式ドキュメント)
公式ドキュメントにsignInWithRedirect()
を使用するベストプラクティスが載っていました。
- Firebase Hostingを使用して
firebaseapp.com
のサブドメインでアプリをホストする場合、この問題による影響はなく、特別な対応は必要ありません(実施したが解決せず)
- Firebase Hostingを使用してカスタムドメインまたは
web.app
のサブドメインでアプリをホストする場合は、「オプション1」を使用します。
- Firebase以外のサービスでアプリをホストしている場合は、「オプション2」,「オプション3」,「オプション4」,「オプション5」 を使用します。
オプション1: カスタムドメインをauthDomainとして使用できるようにFirebase構成を更新する
オプション2:signInWithPopup()
に切り替える
オプション3: firebaseapp.comへのプロキシ認証リクエスト
オプション4: ログインヘルパーコードを自社ドメイン内でホストする
オプション5: プロバイダのログインを独自に処理する
今回はカスタムドメインでなくFirebase Hostingを使用していますが、開発環境ではsignInWithPopup()
を使うというオプション2を採用しました。
signInWithPopup()
はsessionStorage
に依存せず、ページ遷移しないのでNext.js(App Router)とも非常に相性が良いです。
解決策
認証を実装しているAuthContext.js
で次のように、開発環境ではsignInWithPopup()
を使用し、本番環境では異なるドメイン間ではないため、signInWithRedirect()
を使用する方針を取りました。
useEffect()
で認証状態を監視し、redirect方式で戻ってきた場合のエラー処理も実装しました。
"use client";
import { createContext, useContext, useEffect, useState } from "react";
import {
GoogleAuthProvider,
signInWithPopup,
signInWithRedirect,
getRedirectResult,
onAuthStateChanged,
signOut,
} from "firebase/auth";
import { auth } from "@/firebase";
const AuthContext = createContext();
export const useAuth = () => useContext(AuthContext);
export default function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState(null);
const [loading, setLoading] = useState(true);
// 開発環境かどうかを判定
const isLocalhost =
typeof window !== "undefined" &&
(window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1");
const login = async () => {
const provider = new GoogleAuthProvider();
try {
if (isLocalhost) {
// ★ signInWithPopupで対応
console.log("🔹 Using signInWithPopup (localhost dev mode)");
await signInWithPopup(auth, provider);
} else {
console.log("🔹 Using signInWithRedirect (production)");
await signInWithRedirect(auth, provider);
}
} catch (error) {
console.error("Login error:", error);
}
};
const logout = async () => {
await signOut(auth);
};
useEffect(() => {
// 1. 認証状態を監視
const unsubscribe = onAuthStateChanged(auth, (user) => {
setCurrentUser(user);
setLoading(false);
});
// 2. redirect方式で戻ってきた場合の処理
getRedirectResult(auth).catch((err) => {
if (err && err.code !== "auth/no-auth-event") {
console.warn("getRedirectResult error:", err);
}
});
return () => unsubscribe();
}, []);
const value = { currentUser, login, logout };
return (
<AuthContext.Provider value={value}>
{loading ? <p>Loading...</p> : children}
</AuthContext.Provider>
);
}
呼び出し側のログインボタンを押下すると、無事ポップアップウインドウが表示されGoogleアカウントでログインができました!
付記
useAuth()
で取得したcurrentUser
,login
,logout
をアプリ全体で使用できるようにするため、layout.js
で以下のようにAuthProvider
で囲みました。
import AuthProvider from "@/context/AuthContext";
export const metadata = {
title: "TODOアプリ",
description: "Firebase認証付きTODOアプリ",
};
export default function RootLayout({ children }) {
return (
<html lang="ja">
<body>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
);
}
呼び出し側は以下のようになっています。
"use client";
import { useAuth } from "@/context/AuthContext";
export default function Index() {
const { currentUser, login, logout } = useAuth();
const handleLoginButton = () => {
login();
};
const handleLogoutButton = () => {
logout();
};
return (
<>
<div style={{ textAlign: "center" }}>
<h1>TODOアプリケーション</h1>
{currentUser && (
<div>
<h2>ログインしています。</h2>
<button onClick={handleLogoutButton}>ログアウト</button>
</div>
)}
{!currentUser && (
<div>
<h2>ログインしていません。</h2>
<button onClick={handleLoginButton}>ログイン</button>
</div>
)}
</div>
</>
);
}