next-firebase-auth-edge
の紹介おそらく、既存または新しい Next.js アプリケーションに Firebase 認証を追加する方法を探しているときに、この記事を見つけたのでしょう。アプリが成功する可能性を最大限に高める、賢明で偏見のない、将来を見据えた決定を下すことを目指します。next-firebase-auth-edge の作成者として、完全に偏見のない意見を提供することは得意ではないことを認めなければなりませんが、少なくともライブラリを設計する際に採用したアプローチを正当化しようとします。このガイドを読み終える頃には、このアプローチがシンプルで長期的に実行可能であることがおわかりいただけると思います。
長い紹介は省略します。ライブラリのアイデアは、おそらくあなたと似たような状況からインスピレーションを得たものだとだけ言っておきます。それは、Next.js がApp Routerのカナリア バージョンをリリースした頃でした。私は、書き換えと内部リダイレクトに大きく依存するアプリに取り組んでいました。そのために、カスタム Node.js express
サーバー レンダリング Next.js アプリを使用していました。
私たちはApp RouterとServer Componentsに非常に期待していましたが、それが私たちのカスタム サーバーと互換性がないことは認識していました。ミドルウェアは、カスタム Express サーバーの必要性を排除するために活用できる強力な機能のように思えました。代わりに、Next.js の組み込み機能のみに依存して、ユーザーを別のページに動的にリダイレクトおよび書き換えることにしました。
当時はnext-firebase-authを使用していました。このライブラリは大変気に入っていたのですが、認証ロジックがnext.config.js
、 pages/_app.tsx
、 pages/api/login.ts
、 pages/api/logout.ts
各ファイルに分散され、すぐにレガシーと見なされるようになりました。また、このライブラリはミドルウェアと互換性がなかったため、URL を書き換えたり、ユーザーのコンテキストに基づいてリダイレクトしたりすることができませんでした。
そこで検索を始めましたが、驚いたことに、ミドルウェア内で Firebase 認証をサポートするライブラリは見つかりませんでした。なぜでしょうか? あり得ません! Node.js と React で 11 年以上の商用経験を持つソフトウェア エンジニアとして、私はこの難問に取り組む準備をしていました。
そこで、私は始めました。そして答えは明白でした。ミドルウェアはEdge Runtime内で実行されています。Edge Runtime内では、Web Crypto APIと互換性のある Firebase ライブラリは利用できません。私は絶望しました。無力感を覚えました。新しくておしゃれな API を試すために実際に待たなければならないのは、今回が初めてでしょうか? –いいえ。見張っている鍋は決して沸騰しません。私はすぐに泣き止み、 next-firebase-auth 、 firebase-admin 、およびその他のいくつかの JWT 認証ライブラリをリバース エンジニアリングして、Edge Runtime に適合させ始めました。私は、これまでの認証ライブラリで遭遇したすべての問題に対処する機会を捉え、最も軽量で構成が簡単で、将来を見据えた認証ライブラリを作成することを目指しました。
約 2 週間後、 next-firebase-auth-edgeのバージョン0.0.1
が誕生しました。これは堅実な概念実証でしたが、バージョン0.0.1
は使いたくないでしょう。信じてください。
約2 年後、 372 件のコミット、 110 件の問題解決、そして世界中の素晴らしい開発者からの大量の貴重なフィードバックを経て、このライブラリが、私のもう一人の自分が承認してくれる段階に達したことをお知らせできることを嬉しく思います。
このガイドでは、 next-firebase-auth-edgeのバージョン 1.4.1を使用して、認証済みの Next.js アプリを最初から作成します。新しい Firebase プロジェクトと Next.js アプリの作成から始まり、 next-firebase-auth-edge
とfirebase/auth
ライブラリとの統合まで、各ステップを詳細に説明します。このチュートリアルの最後に、アプリを Vercel にデプロイして、ローカルでも本番環境でもすべてが機能していることを確認します。
この部分では、Firebase 認証をまだ設定していないことを前提としています。そうでない場合は、次の部分に進んでください。
Firebaseコンソールにアクセスしてプロジェクトを作成しましょう
プロジェクトを作成したら、Firebase認証を有効にしましょう。コンソールを開き、ビルド > 認証 > サインイン方法の順に進み、メールとパスワードによる方法を有効にします。これがアプリでサポートする方法です。
最初のサインイン方法を有効にすると、プロジェクトでFirebase認証が有効になり、プロジェクト設定でWeb APIキーを取得できるようになります。
API キーをコピーして安全に保管してください。次に、次のタブ「クラウド メッセージング」を開き、送信者 IDを書き留めておきます。これは後で必要になります。
最後に、サービス アカウントの認証情報を生成する必要があります。これにより、アプリは Firebase サービスに完全にアクセスできるようになります。 [プロジェクト設定] > [サービス アカウント]に移動し、 [新しい秘密キーを生成]をクリックします。これにより、サービス アカウントの認証情報を含む.json
ファイルがダウンロードされます。このファイルを既知の場所に保存します。
これで完了です。Next.jsアプリを Firebase 認証と統合する準備ができました。
このガイドでは、 Node.jsとnpm がインストールされていることを前提としています。このチュートリアルで使用されているコマンドは、最新の LTS Node.js v20に対して検証されています。ターミナルでnode -v
を実行すると、ノードのバージョンを確認できます。また、 NVMなどのツールを使用して、Node.js のバージョンをすばやく切り替えることもできます。
お気に入りのターミナルを開き、プロジェクトフォルダに移動して実行します。
npx create-next-app@latest
シンプルにするために、デフォルトの設定を使用します。つまり、 TypeScript
とtailwind
を使用します。
✔ What is your project named? … my-app ✔ Would you like to use TypeScript? … Yes ✔ Would you like to use ESLint? … Yes ✔ Would you like to use Tailwind CSS? … Yes ✔ Would you like to use `src/` directory? … No ✔ Would you like to use App Router? (recommended) … Yes ✔ Would you like to customize the default import alias (@/*)? … No
プロジェクトのルートディレクトリに移動して、すべての依存関係がインストールされていることを確認しましょう。
cd my-app npm install
すべてが期待どおりに動作することを確認するために、 npm run dev
コマンドで Next.js 開発サーバーを起動しましょう。 http://localhost:3000を開くと、次のような Next.js のウェルカム ページが表示されます。
Firebase との統合を始める前に、Firebase 構成を安全に保存して読み取る方法が必要です。幸いなことに、Next.js には組み込みのdotenvサポートが付属しています。
お気に入りのコードエディタを開き、プロジェクトフォルダに移動します
プロジェクトのルート ディレクトリに.env.local
ファイルを作成し、次の環境変数を入力します。
FIREBASE_ADMIN_CLIENT_EMAIL=... FIREBASE_ADMIN_PRIVATE_KEY=... AUTH_COOKIE_NAME=AuthToken AUTH_COOKIE_SIGNATURE_KEY_CURRENT=secret1 AUTH_COOKIE_SIGNATURE_KEY_PREVIOUS=secret2 USE_SECURE_COOKIES=false NEXT_PUBLIC_FIREBASE_PROJECT_ID=... NEXT_PUBLIC_FIREBASE_API_KEY=AIza... NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=....firebaseapp.com NEXT_PUBLIC_FIREBASE_DATABASE_URL=....firebaseio.com NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=...
NEXT_PUBLIC_
で始まる変数はクライアント側のバンドルで使用できることに注意してください。Firebase Auth Client SDKを設定するにはこれらが必要になります。
NEXT_PUBLIC_FIREBASE_PROJECT_ID
、 FIREBASE_ADMIN_CLIENT_EMAIL
、 FIREBASE_ADMIN_PRIVATE_KEY
は、サービス アカウント資格情報を生成した後にダウンロードした.json
ファイルから取得できます。
AUTH_COOKIE_NAME
、ユーザーの資格情報を保存するために使用されるCookieの名前になります。
AUTH_COOKIE_SIGNATURE_KEY_CURRENT
とAUTH_COOKIE_SIGNATURE_KEY_PREVIOUS
は、認証情報に署名する際に使用する秘密鍵です。
NEXT_PUBLIC_FIREBASE_API_KEY
はプロジェクト設定の一般ページから取得したWeb API キーです。
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN
はyour-project-id .firebaseapp.com です
NEXT_PUBLIC_FIREBASE_DATABASE_URL
はyour-project-id .firebaseio.com です
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID
は、プロジェクト設定>クラウドメッセージングページから取得できます。
USE_SECURE_COOKIES
ローカル開発には使用されませんが、アプリをVercelにデプロイするときに役立ちます。
next-firebase-auth-edge
インストールと初期設定npm install next-firebase-auth-edge@^1.4.1
を実行して、ライブラリをプロジェクトの依存関係に追加します。
プロジェクト構成をカプセル化するためにconfig.ts
ファイルを作成しましょう。必須ではありませんが、コード例が読みやすくなります。
これらの値について考えることにあまり時間をかけないでください。後で詳しく説明します。
export const serverConfig = { cookieName: process.env.AUTH_COOKIE_NAME!, cookieSignatureKeys: [process.env.AUTH_COOKIE_SIGNATURE_KEY_CURRENT!, process.env.AUTH_COOKIE_SIGNATURE_KEY_PREVIOUS!], cookieSerializeOptions: { path: "/", httpOnly: true, secure: process.env.USE_SECURE_COOKIES === "true", sameSite: "lax" as const, maxAge: 12 * 60 * 60 * 24, }, serviceAccount: { projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!, clientEmail: process.env.FIREBASE_ADMIN_CLIENT_EMAIL!, privateKey: process.env.FIREBASE_ADMIN_PRIVATE_KEY?.replace(/\\n/g, "\n")!, } }; export const clientConfig = { projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!, authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL, messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID };
プロジェクトのルートにmiddleware.ts
ファイルを作成し、以下を貼り付けます。
import { NextRequest } from "next/server"; import { authMiddleware } from "next-firebase-auth-edge"; import { clientConfig, serverConfig } from "./config"; export async function middleware(request: NextRequest) { return authMiddleware(request, { loginPath: "/api/login", logoutPath: "/api/logout", apiKey: clientConfig.apiKey, cookieName: serverConfig.cookieName, cookieSignatureKeys: serverConfig.cookieSignatureKeys, cookieSerializeOptions: serverConfig.cookieSerializeOptions, serviceAccount: serverConfig.serviceAccount, }); } export const config = { matcher: [ "/", "/((?!_next|api|.*\\.).*)", "/api/login", "/api/logout", ], };
信じられないかもしれませんが、私たちはアプリのサーバーを Firebase 認証と統合しました。実際に使用する前に、構成について少し説明しましょう。
loginPath
、authMiddleware にGET /api/login
エンドポイントを公開するように指示します。このエンドポイントがAuthorization: Bearer ${idToken}
* ヘッダーで呼び出されると、署名されたカスタム トークンとリフレッシュ トークンを含む HTTP(S)-Only Set-Cookie
ヘッダーで応答します。
* idToken
、Firebase Client SDKで利用可能なgetIdToken
関数を使用して取得されます。これについては後で詳しく説明します。
同様に、 logoutPath
ミドルウェアにGET /api/logout
を公開するように指示しますが、追加のヘッダーは必要ありません。呼び出されると、ブラウザから認証 Cookie が削除されます。
apiKey
は Web API キーです。ミドルウェアはこれを使用してカスタム トークンを更新し、資格情報の有効期限が切れた後に認証 Cookie をリセットします。
cookieName
/api/login
および/api/logout
エンドポイントによって設定および削除される Cookie の名前です。
cookieSignatureKeys
、ユーザー認証情報に署名する秘密鍵のリストです。認証情報は常にリストの最初の鍵で署名されるため、少なくとも 1 つの値を指定する必要があります。複数の鍵を指定して鍵のローテーションを実行することもできます。
cookieSerializeOptions
Set-Cookie
ヘッダーを生成するときにCookieに渡されるオプションです。詳細については、 Cookie のREADME を参照してください。
serviceAccount
、ライブラリが Firebase サービスを使用することを許可します。
マッチャーは、Next.js サーバーに/api/login
、 /api/logout
、 /
、およびファイルや API 呼び出しではないその他のパスに対してミドルウェアを実行するように指示します。
export const config = { matcher: [ "/", "/((?!_next|api|.*\\.).*)", "/api/login", "/api/logout", ], };
すべての/api/*
呼び出しに対してミドルウェアを有効にしないのはなぜかと疑問に思うかもしれません。有効にすることはできますが、認証されていない呼び出しは API ルート ハンドラー自体で処理するのが良い方法です。これはこのチュートリアルの範囲外ですが、興味があればお知らせください。いくつか例を用意します。
ご覧のとおり、構成は最小限で、目的が明確に定義されています。では、 /api/login
および/api/logout
エンドポイントの呼び出しを開始しましょう。
できるだけシンプルにするために、デフォルトのNext.jsホームページをクリアして、パーソナライズされたコンテンツに置き換えてみましょう。
./app/page.tsx
を開き、これを貼り付けます:
import { getTokens } from "next-firebase-auth-edge"; import { cookies } from "next/headers"; import { notFound } from "next/navigation"; import { clientConfig, serverConfig } from "../config"; export default async function Home() { const tokens = await getTokens(cookies(), { apiKey: clientConfig.apiKey, cookieName: serverConfig.cookieName, cookieSignatureKeys: serverConfig.cookieSignatureKeys, serviceAccount: serverConfig.serviceAccount, }); if (!tokens) { notFound(); } return ( <main className="flex min-h-screen flex-col items-center justify-center p-24"> <h1 className="text-xl mb-4">Super secure home page</h1> <p> Only <strong>{tokens?.decodedToken.email}</strong> holds the magic key to this kingdom! </p> </main> ); }
これを少しずつ分解してみましょう。
getTokens
関数は、 Cookieからユーザー資格情報を検証して抽出するように設計されています。
const tokens = await getTokens(cookies(), { apiKey: clientConfig.apiKey, cookieName: serverConfig.cookieName, cookieSignatureKeys: serverConfig.cookieSignatureKeys, serviceAccount: serverConfig.serviceAccount, });
ユーザーが認証されていない場合はnull
で解決され、そうでない場合は次の 2 つのプロパティを含むオブジェクトで解決されます。
token
は、外部バックエンド サービスへの API リクエストを承認するために使用できる idToken string
です。これは少し範囲外ですが、ライブラリによって分散サービス アーキテクチャが可能になることは言及する価値があります。 token
、すべてのプラットフォームのすべての公式 Firebase ライブラリと互換性があり、すぐに使用できます。
decodedToken
、名前が示すように、 token
のデコードされたバージョンであり、電子メール アドレス、プロフィール写真、カスタム クレームなど、ユーザーを識別するために必要なすべての情報が含まれており、これにより、ロールと権限に基づいてアクセスをさらに制限できるようになります。
tokens
を取得した後、 next/navigation
のnotFound関数を使用して、認証されたユーザーのみがページにアクセスできるようにします。
if (!tokens) { notFound(); }
最後に、基本的なパーソナライズされたユーザーコンテンツをレンダリングします。
<main className="flex min-h-screen flex-col items-center justify-center p-24"> <h1 className="text-xl mb-4">Super secure home page</h1> <p> Only <strong>{tokens?.decodedToken.email}</strong> holds the magic key to this kingdom!" </p> </main>
実行してみましょう。
開発サーバーを閉じてしまった場合は、 npm run dev
を実行するだけです。
http://localhost:3000/ にアクセスしようとすると、 「404: このページが見つかりません」と表示されます。
成功しました!私たちは詮索好きな目から秘密を守りました!
firebase
をインストールし、Firebase クライアント SDK を初期化するプロジェクトのルートディレクトリでnpm install firebase
実行します。
クライアントSDKをインストールしたら、プロジェクトのルートディレクトリにfirebase.ts
ファイルを作成し、次の内容を貼り付けます。
import { initializeApp } from 'firebase/app'; import { clientConfig } from './config'; export const app = initializeApp(clientConfig);
これにより、Firebase Client SDKが初期化され、クライアントコンポーネントのアプリオブジェクトが公開されます。
誰にも見られなければ、超安全なホームページを持つ意味がありません。アプリに人々がアクセスできるように、簡単な登録ページを作成しましょう。
./app/register/page.tsx
の下に新しいおしゃれなページを作成しましょう。
"use client"; import { FormEvent, useState } from "react"; import Link from "next/link"; import { getAuth, createUserWithEmailAndPassword } from "firebase/auth"; import { app } from "../../firebase"; import { useRouter } from "next/navigation"; export default function Register() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmation, setConfirmation] = useState(""); const [error, setError] = useState(""); const router = useRouter(); async function handleSubmit(event: FormEvent) { event.preventDefault(); setError(""); if (password !== confirmation) { setError("Passwords don't match"); return; } try { await createUserWithEmailAndPassword(getAuth(app), email, password); router.push("/login"); } catch (e) { setError((e as Error).message); } } return ( <main className="flex min-h-screen flex-col items-center justify-center p-8"> <div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700"> <div className="p-6 space-y-4 md:space-y-6 sm:p-8"> <h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white"> Pray tell, who be this gallant soul seeking entry to mine humble abode? </h1> <form onSubmit={handleSubmit} className="space-y-4 md:space-y-6" action="#" > <div> <label htmlFor="email" className="block mb-2 text-sm font-medium text-gray-900 dark:text-white" > Your email </label> <input type="email" name="email" value={email} onChange={(e) => setEmail(e.target.value)} id="email" className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="[email protected]" required /> </div> <div> <label htmlFor="password" className="block mb-2 text-sm font-medium text-gray-900 dark:text-white" > Password </label> <input type="password" name="password" value={password} onChange={(e) => setPassword(e.target.value)} id="password" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required /> </div> <div> <label htmlFor="confirm-password" className="block mb-2 text-sm font-medium text-gray-900 dark:text-white" > Confirm password </label> <input type="password" name="confirm-password" value={confirmation} onChange={(e) => setConfirmation(e.target.value)} id="confirm-password" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required /> </div> {error && ( <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert" > <span className="block sm:inline">{error}</span> </div> )} <button type="submit" className="w-full text-white bg-gray-600 hover:bg-gray-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-primary-800" > Create an account </button> <p className="text-sm font-light text-gray-500 dark:text-gray-400"> Already have an account?{" "} <Link href="/login" className="font-medium text-gray-600 hover:underline dark:text-gray-500" > Login here </Link> </p> </form> </div> </div> </main> ); }
わかっています。文章が長いですが、我慢してください。
"use client";
登録ページがクライアント側APIを使用することを示します。
const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmation, setConfirmation] = useState(""); const [error, setError] = useState("");
次に、フォームの状態を保持するための変数とセッターを定義します。
const router = useRouter(); async function handleSubmit(event: FormEvent) { event.preventDefault(); setError(""); if (password !== confirmation) { setError("Passwords don't match"); return; } try { await createUserWithEmailAndPassword(getAuth(app), email, password); router.push("/login"); } catch (e) { setError((e as Error).message); } }
ここでは、フォーム送信ロジックを定義します。まず、 password
とconfirmation
等しいかどうかを検証し、等しくない場合はエラー状態を更新します。値が有効な場合は、 firebase/auth
からcreateUserWithEmailAndPassword
を使用してユーザー アカウントを作成します。この手順が失敗した場合 (例: 電子メールが取得された場合)、エラーを更新してユーザーに通知します。
すべてがうまくいけば、ユーザーを/login
ページにリダイレクトします。おそらく今は混乱しているでしょうが、それは当然です。 /login
ページはまだ存在していません。私たちは次に何が起こるかを準備しているだけです。
http://localhost:3000/registerにアクセスすると、ページはおおよそ次のようになります。
ユーザーが登録できるようになったので、身元を証明しましょう
./app/login/page.tsx
の下にログインページを作成します。
"use client"; import { FormEvent, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { getAuth, signInWithEmailAndPassword } from "firebase/auth"; import { app } from "../../firebase"; export default function Login() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const router = useRouter(); async function handleSubmit(event: FormEvent) { event.preventDefault(); setError(""); try { const credential = await signInWithEmailAndPassword( getAuth(app), email, password ); const idToken = await credential.user.getIdToken(); await fetch("/api/login", { headers: { Authorization: `Bearer ${idToken}`, }, }); router.push("/"); } catch (e) { setError((e as Error).message); } } return ( <main className="flex min-h-screen flex-col items-center justify-center p-8"> <div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700"> <div className="p-6 space-y-4 md:space-y-6 sm:p-8"> <h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white"> Speak thy secret word! </h1> <form onSubmit={handleSubmit} className="space-y-4 md:space-y-6" action="#" > <div> <label htmlFor="email" className="block mb-2 text-sm font-medium text-gray-900 dark:text-white" > Your email </label> <input type="email" name="email" value={email} onChange={(e) => setEmail(e.target.value)} id="email" className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="[email protected]" required /> </div> <div> <label htmlFor="password" className="block mb-2 text-sm font-medium text-gray-900 dark:text-white" > Password </label> <input type="password" name="password" value={password} onChange={(e) => setPassword(e.target.value)} id="password" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required /> </div> {error && ( <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert" > <span className="block sm:inline">{error}</span> </div> )} <button type="submit" className="w-full text-white bg-gray-600 hover:bg-gray-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-primary-800" > Enter </button> <p className="text-sm font-light text-gray-500 dark:text-gray-400"> Don't have an account?{" "} <Link href="/register" className="font-medium text-gray-600 hover:underline dark:text-gray-500" > Register here </Link> </p> </form> </div> </div> </main> ); }
ご覧のとおり、登録ページと非常によく似ています。重要な部分に注目してみましょう。
async function handleSubmit(event: FormEvent) { event.preventDefault(); setError(""); try { const credential = await signInWithEmailAndPassword( getAuth(app), email, password ); const idToken = await credential.user.getIdToken(); await fetch("/api/login", { headers: { Authorization: `Bearer ${idToken}`, }, }); router.push("/"); } catch (e) { setError((e as Error).message); } }
ここですべての魔法が起こります。 firebase/auth
のsignInEmailAndPassword
を使用して、ユーザーのidToken
を取得します。
次に、ミドルウェアによって公開される/api/login
エンドポイントを呼び出します。このエンドポイントは、ユーザーの資格情報を使用してブラウザの Cookie を更新します。
最後に、 router.push("/");
を呼び出してユーザーをホームページにリダイレクトします。
ログインページはおおよそ次のようになります
試してみましょう!
http://localhost:3000/registerにアクセスし、ランダムなメールアドレスとパスワードを入力してアカウントを作成します。これらの資格情報はhttp://localhost:3000/loginページで使用します。Enterをクリックすると、非常に安全なホームページにリダイレクトされます。
ついに、私たち自身の、個人的な、超安全なホームページを見ることができました! でも待ってください! どうやって抜け出すのでしょうか?
永久に(または 12 日間)世界から締め出されないように、ログアウト ボタンを追加する必要があります。
始める前に、Firebase Client SDK を使用してサインアウトできるクライアント コンポーネントを作成する必要があります。
./app/HomePage.tsx
の下に新しいファイルを作成しましょう
"use client"; import { useRouter } from "next/navigation"; import { getAuth, signOut } from "firebase/auth"; import { app } from "../firebase"; interface HomePageProps { email?: string; } export default function HomePage({ email }: HomePageProps) { const router = useRouter(); async function handleLogout() { await signOut(getAuth(app)); await fetch("/api/logout"); router.push("/login"); } return ( <main className="flex min-h-screen flex-col items-center justify-center p-24"> <h1 className="text-xl mb-4">Super secure home page</h1> <p className="mb-8"> Only <strong>{email}</strong> holds the magic key to this kingdom! </p> <button onClick={handleLogout} className="text-white bg-gray-600 hover:bg-gray-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-primary-800" > Logout </button> </main> ); }
お気づきかもしれませんが、これは./app/page.tsx
を少し変更したバージョンです。getTokens getTokens
サーバー コンポーネントとAPI ルート ハンドラー内でのみ動作しますが、 signOut
とuseRouter
クライアント コンテキストで実行する必要があるため、別のクライアント コンポーネントを作成する必要がありました。少し複雑ですが、実際には非常に強力です。後で説明します。
ログアウトのプロセスに注目しましょう
const router = useRouter(); async function handleLogout() { await signOut(getAuth(app)); await fetch("/api/logout"); router.push("/login"); }
まず、Firebase Client SDK からサインアウトします。次に、ミドルウェアによって公開された/api/logout
エンドポイントを呼び出します。最後に、ユーザーを/login
ページにリダイレクトします。
サーバーのホームページを更新しましょう./app/page.tsx
に移動して、次のコードを貼り付けます。
import { getTokens } from "next-firebase-auth-edge"; import { cookies } from "next/headers"; import { notFound } from "next/navigation"; import { clientConfig, serverConfig } from "../config"; import HomePage from "./HomePage"; export default async function Home() { const tokens = await getTokens(cookies(), { apiKey: clientConfig.apiKey, cookieName: serverConfig.cookieName, cookieSignatureKeys: serverConfig.cookieSignatureKeys, serviceAccount: serverConfig.serviceAccount, }); if (!tokens) { notFound(); } return <HomePage email={tokens?.decodedToken.email} />; }
現在、 Home
サーバー コンポーネントは、ユーザー トークンを取得してHomePage
クライアント コンポーネントに渡すことだけを担当しています。これは、実際には非常に一般的で便利なパターンです。
これをテストしてみましょう:
できました! これで、自分の意志でアプリケーションにログインしたりログアウトしたりできるようになりました。完璧です!
またはそれは?
認証されていないユーザーがhttp://localhost:3000/を開いてホームページに入ろうとすると、404: このページが見つかりません が表示されます。
また、認証されたユーザーは、ログアウトしなくても、http://localhost:3000/registerページとhttp://localhost:3000/loginページにアクセスできます。
もっとうまくできるはずだ。
リダイレクト ロジックを追加する必要があるようです。いくつかのルールを定義しましょう。
/register
および/login
ページにアクセスしようとすると、 /
にリダイレクトする必要があります。/
ページにアクセスしようとすると、 /login
にリダイレクトする必要があります。
ミドルウェアは、Next.js アプリでリダイレクトを処理するための最良の方法の 1 つです。幸いなことに、 authMiddleware
、さまざまなリダイレクト シナリオを処理するための多数のオプションとヘルパー関数をサポートしています。
middleware.ts
ファイルを開いて、この更新バージョンを貼り付けましょう
import { NextRequest, NextResponse } from "next/server"; import { authMiddleware, redirectToHome, redirectToLogin } from "next-firebase-auth-edge"; import { clientConfig, serverConfig } from "./config"; const PUBLIC_PATHS = ['/register', '/login']; export async function middleware(request: NextRequest) { return authMiddleware(request, { loginPath: "/api/login", logoutPath: "/api/logout", apiKey: clientConfig.apiKey, cookieName: serverConfig.cookieName, cookieSignatureKeys: serverConfig.cookieSignatureKeys, cookieSerializeOptions: serverConfig.cookieSerializeOptions, serviceAccount: serverConfig.serviceAccount, handleValidToken: async ({token, decodedToken}, headers) => { if (PUBLIC_PATHS.includes(request.nextUrl.pathname)) { return redirectToHome(request); } return NextResponse.next({ request: { headers } }); }, handleInvalidToken: async (reason) => { console.info('Missing or malformed credentials', {reason}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); }, handleError: async (error) => { console.error('Unhandled authentication error', {error}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); } }); } export const config = { matcher: [ "/", "/((?!_next|api|.*\\.).*)", "/api/login", "/api/logout", ], };
これで完了です。リダイレクト ルールはすべて実装しました。詳しく見ていきましょう。
const PUBLIC_PATHS = ['/register', '/login'];
handleValidToken: async ({token, decodedToken}, headers) => { if (PUBLIC_PATHS.includes(request.nextUrl.pathname)) { return redirectToHome(request); } return NextResponse.next({ request: { headers } }); },
handleValidToken
、有効なユーザー資格情報がリクエストに添付されているとき、つまりユーザーが認証されているときに呼び出されます。これは、最初の引数としてtokens
オブジェクト、2 番目の引数として変更されたリクエスト ヘッダーを使用して呼び出されます。これはNextResponse
で解決される必要があります。
next-firebase-auth-edge
のredirectToHome
NextResponse.redirect(new URL(“/“))
に簡略化できるオブジェクトを返すヘルパー関数です。
PUBLIC_PATHS.includes(request.nextUrl.pathname)
をチェックすることで、認証されたユーザーが/login
または/register
ページにアクセスしようとしているかどうかを検証し、その場合はホームにリダイレクトします。
handleInvalidToken: async (reason) => { console.info('Missing or malformed credentials', {reason}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); },
handleInvalidToken
、予期される事態が発生したときに呼び出されます。予期されるイベントの 1 つは、ユーザーが別のデバイスから初めてアプリを表示する場合や、資格情報の有効期限が切れた場合です。
認証されていないユーザーに対してhandleInvalidToken
が呼び出されることがわかったので、2番目のルールに進むことができます。認証されていないユーザーが/
ページにアクセスしようとすると、 /login
にリダイレクトする必要があります。
他に満たすべき条件がないので、単にredirectToLogin
の結果を返します。これはNextResponse.redirect(new URL(“/login”))
と簡略化できます。これにより、ユーザーがリダイレクト ループに陥らないことも保証されます。
最後に、
handleError: async (error) => { console.error('Unhandled authentication error', {error}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); }
handleInvalidToken
とは対照的に、 handleError
予期しない*事態が発生し、調査が必要な場合に呼び出されます。ドキュメントには、考えられるエラーとその説明の一覧が記載されています。
エラーが発生した場合、その事実を記録し、ユーザーをログインページに安全にリダイレクトします。
* Google 公開鍵が更新された後、 INVALID_ARGUMENT
エラーでhandleError
が呼び出されることがあります。
これはキーローテーションの一種であり、想定されています。 詳細については、この Github の問題を参照してください。
さあ、これで終わりです。ついに。
Web アプリからログアウトして、 http://localhost:3000/ /login
開きます。/login ページにリダイレクトされるはずです。
もう一度ログインして、 http://localhost:3000/loginを入力し/
みます。/ ページにリダイレクトされるはずです。
シームレスなユーザー エクスペリエンスを提供しただけではありません。next next-firebase-auth-edge
アプリのサーバーでのみ動作し、追加のクライアント側コードを導入しない、バンドル サイズのゼロライブラリです。結果として得られるバンドルは、まさに最小限です。これが私が完璧と呼ぶものです。
これで、アプリはサーバー コンポーネントとクライアント コンポーネントの両方で Firebase 認証と完全に統合されました。Next.js の可能性を最大限に引き出す準備が整いました。
アプリのソースコードはnext-firebase-auth-edge/examples/next-typescript-minimalにあります。
このガイドでは、新しい Next.js アプリと Firebase 認証の統合について説明しました。
この記事は非常に広範囲にわたりますが、パスワード リセット フォームや、電子メールとパスワード以外のサインイン方法など、認証フローの重要な部分が省略されています。
ライブラリに興味がある場合は、本格的なnext-firebase-auth-edge スターター デモ ページをプレビューできます。
Firestore統合、サーバーアクション、 App-Checkサポートなどを備えています。
ライブラリには、多数の例を含む専用のドキュメントページが用意されています。
この記事が気に入っていただけましたら、 next-firebase-auth-edgeリポジトリにスターを付けていただけると嬉しいです。よろしくお願いします! 🎉
このボーナスガイドでは、Next.jsアプリをVercelにデプロイする方法を説明します。
Vercel にデプロイするには、新しいアプリのリポジトリを作成する必要があります。
https://github.com/にアクセスして、新しいリポジトリを作成します。
create-next-app
すでにローカルの Git リポジトリを開始しているので、プロジェクトのルート フォルダーに移動して以下を実行するだけです。
git add --all git commit -m "first commit" git branch -M main git remote add origin [email protected]:path-to-your-new-github-repository.git git push -u origin main
https://vercel.com/にアクセスし、Github アカウントでサインインします。
ログインしたら、Vercelの概要ページに移動し、 「新規追加」>「プロジェクト」をクリックします。
先ほど作成した Github リポジトリの横にある[インポート] をクリックします。まだデプロイしないでください。
デプロイする前に、プロジェクト構成を提供する必要があります。環境変数をいくつか追加しましょう。
VercelはデフォルトでHTTPSを使用するため、 USE_SECURE_COOKIES
true
に設定することを忘れないでください。
これで、 「デプロイ」をクリックする準備ができました
1、2 分待つと、次のような URL でアプリにアクセスできるようになります: https://next-typescript-minimal-xi.vercel.app/
完了です。こんなに簡単だとは思わなかったでしょう。
このガイドが気に入ったら、 next-firebase-auth-edgeリポジトリにスターを付けていただけると幸いです。
コメント欄でフィードバックをお送りください。よろしくお願いします! 🎉