paint-brush
最新の Next.js 機能で Firebase 認証を使用する@awinogrodzki
5,208 測定値
5,208 測定値

最新の Next.js 機能で Firebase 認証を使用する

Amadeusz Winogrodzki32m2024/04/04
Read on Terminal Reader

長すぎる; 読むには

バンドルサイズがゼロの `next-firebase-auth-edge` ライブラリを使用して、Firebase Authentication を Next.js に統合するための包括的なステップバイステップ ガイドです。ユーザー登録、ログイン、ログアウト機能の手順と、シームレスなユーザー エクスペリエンスを実現するリダイレクト ロジックが含まれています。 このガイドでは、Firebase Authentication を最新の Next.js 機能 (App Router、ミドルウェア、サーバー コンポーネントなど) に統合する方法を学びます。 最後に、アプリを Vercel にデプロイする手順を説明し、Firebase Authentication を使用して Next.js アプリケーションを強化したい開発者向けに、ライブラリの使いやすさと将来を見据えた設計を紹介します。
featured image - 最新の Next.js 機能で Firebase 認証を使用する
Amadeusz Winogrodzki HackerNoon profile picture
0-item
1-item

next-firebase-auth-edgeの紹介

おそらく、既存または新しい Next.js アプリケーションに Firebase 認証を追加する方法を探しているときに、この記事を見つけたのでしょう。アプリが成功する可能性を最大限に高める、賢明で偏見のない、将来を見据えた決定を下すことを目指します。next-firebase-auth-edge の作成者として、完全に偏見のない意見を提供することは得意ではないことを認めなければなりませんが、少なくともライブラリを設計する際に採用したアプローチを正当化しようとします。このガイドを読み終える頃には、このアプローチがシンプルで長期的に実行可能であることがおわかりいただけると思います。


始まり

長い紹介は省略します。ライブラリのアイデアは、おそらくあなたと似たような状況からインスピレーションを得たものだとだけ言っておきます。それは、Next.js がApp Routerのカナリア バージョンをリリースした頃でした。私は、書き換えと内部リダイレクトに大きく依存するアプリに取り組んでいました。そのために、カスタム Node.js expressサーバー レンダリング Next.js アプリを使用していました。


私たちはApp RouterServer Componentsに非常に期待していましたが、それが私たちのカスタム サーバーと互換性がないことは認識していました。ミドルウェアは、カスタム Express サーバーの必要性を排除するために活用できる強力な機能のように思えました。代わりに、Next.js の組み込み機能のみに依存して、ユーザーを別のページに動的にリダイレクトおよび書き換えることにしました。


当時はnext-firebase-authを使用していました。このライブラリは大変気に入っていたのですが、認証ロジックがnext.config.jspages/_app.tsxpages/api/login.tspages/api/logout.ts各ファイルに分散され、すぐにレガシーと見なされるようになりました。また、このライブラリはミドルウェアと互換性がなかったため、URL を書き換えたり、ユーザーのコンテキストに基づいてリダイレクトしたりすることができませんでした。


そこで検索を始めましたが、驚いたことに、ミドルウェア内で Firebase 認証をサポートするライブラリは見つかりませんでした。なぜでしょうか? あり得ません! Node.js と React で 11 年以上の商用経験を持つソフトウェア エンジニアとして、私はこの難問に取り組む準備をしていました。


そこで、私は始めました。そして答えは明白でした。ミドルウェアはEdge Runtime内で実行されています。Edge Runtime内では、Web Crypto APIと互換性のある Firebase ライブラリは利用できません。私は絶望しました。無力感を覚えました。新しくておしゃれな API を試すために実際に待たなければならないのは、今回が初めてでしょうか? –いいえ。見張っている鍋は決して沸騰しません。私はすぐに泣き止み、 next-firebase-authfirebase-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-edgefirebase/authライブラリとの統合まで、各ステップを詳細に説明します。このチュートリアルの最後に、アプリを Vercel にデプロイして、ローカルでも本番環境でもすべてが機能していることを確認します。


Firebaseの設定

この部分では、Firebase 認証をまだ設定していないことを前提としています。そうでない場合は、次の部分に進んでください。


Firebaseコンソールにアクセスしてプロジェクトを作成しましょう


プロジェクトを作成したら、Firebase認証を有効にしましょう。コンソールを開き、ビルド > 認証 > サインイン方法の順に進み、メールとパスワードによる方法を有効にします。これがアプリでサポートする方法です。


メール/パスワードによるサインイン方法を有効にする


最初のサインイン方法を有効にすると、プロジェクトでFirebase認証が有効になり、プロジェクト設定Web APIキーを取得できるようになります。


Web APIキーを取得する


API キーをコピーして安全に保管してください。次に、次のタブ「クラウド メッセージング」を開き、送信者 IDを書き留めておきます。これは後で必要になります。


送信者IDを取得する


最後に、サービス アカウントの認証情報を生成する必要があります。これにより、アプリは Firebase サービスに完全にアクセスできるようになります。 [プロジェクト設定] > [サービス アカウント]に移動し、 [新しい秘密キーを生成]をクリックします。これにより、サービス アカウントの認証情報を含む.jsonファイルがダウンロードされます。このファイルを既知の場所に保存します。



これで完了です。Next.jsアプリを Firebase 認証と統合する準備ができました。

Next.js アプリをゼロから作成する

このガイドでは、 Node.jsnpm がインストールされていることを前提としています。このチュートリアルで使用されているコマンドは、最新の LTS Node.js v20に対して検証されています。ターミナルでnode -vを実行すると、ノードのバージョンを確認できます。また、 NVMなどのツールを使用して、Node.js のバージョンをすばやく切り替えることもできます。

CLI で Next.js アプリをセットアップする

お気に入りのターミナルを開き、プロジェクトフォルダに移動して実行します。

 npx create-next-app@latest


シンプルにするために、デフォルトの設定を使用します。つまり、 TypeScripttailwindを使用します。

 ✔ 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 のウェルカム ページが表示されます。


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_IDFIREBASE_ADMIN_CLIENT_EMAILFIREBASE_ADMIN_PRIVATE_KEYは、サービス アカウント資格情報を生成した後にダウンロードした.jsonファイルから取得できます。


AUTH_COOKIE_NAME 、ユーザーの資格情報を保存するために使用されるCookieの名前になります。

AUTH_COOKIE_SIGNATURE_KEY_CURRENTAUTH_COOKIE_SIGNATURE_KEY_PREVIOUSは、認証情報に署名する際に使用する秘密鍵です。


NEXT_PUBLIC_FIREBASE_API_KEYプロジェクト設定の一般ページから取得したWeb API キーです。

NEXT_PUBLIC_FIREBASE_AUTH_DOMAINyour-project-id .firebaseapp.com です

NEXT_PUBLIC_FIREBASE_DATABASE_URLyour-project-id .firebaseio.com です

NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_IDは、プロジェクト設定>クラウドメッセージングページから取得できます。


USE_SECURE_COOKIESローカル開発には使用されませんが、アプリをVercelにデプロイするときに役立ちます。

Firebase認証との統合

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/navigationnotFound関数を使用して、認証されたユーザーのみがページにアクセスできるようにします。

 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); } }

ここでは、フォーム送信ロジックを定義します。まず、 passwordconfirmation等しいかどうかを検証し、等しくない場合はエラー状態を更新します。値が有効な場合は、 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&apos;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/authsignInEmailAndPasswordを使用して、ユーザーの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 ルート ハンドラー内でのみ動作しますが、 signOutuseRouterクライアント コンテキストで実行する必要があるため、別のクライアント コンポーネントを作成する必要がありました。少し複雑ですが、実際には非常に強力です。後で説明します。


ログアウトのプロセスに注目しましょう

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-edgeredirectToHome 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リポジトリにスターを付けていただけると嬉しいです。よろしくお願いします! 🎉



ボーナス – Vercel へのアプリのデプロイ

このボーナスガイドでは、Next.jsアプリをVercelにデプロイする方法を説明します。

Gitリポジトリの作成

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


新しい Vercel プロジェクトの追加

https://vercel.com/にアクセスし、Github アカウントでサインインします。


ログインしたら、Vercelの概要ページに移動し、 「新規追加」>「プロジェクト」をクリックします。


先ほど作成した Github リポジトリの横にある[インポート] をクリックします。まだデプロイしないでください。


デプロイする前に、プロジェクト構成を提供する必要があります。環境変数をいくつか追加しましょう。

Vercelへの導入


VercelはデフォルトでHTTPSを使用するため、 USE_SECURE_COOKIES trueに設定することを忘れないでください。


これで、 「デプロイ」をクリックする準備ができました


1、2 分待つと、次のような URL でアプリにアクセスできるようになります: https://next-typescript-minimal-xi.vercel.app/


完了です。こんなに簡単だとは思わなかったでしょう。




このガイドが気に入ったら、 next-firebase-auth-edgeリポジトリにスターを付けていただけると幸いです。


コメント欄でフィードバックをお送りください。よろしくお願いします! 🎉