next-firebase-auth-edge
Sie haben diesen Artikel wahrscheinlich gefunden, als Sie nach Möglichkeiten gesucht haben, die Firebase-Authentifizierung zu Ihrer bestehenden oder neuen Next.js-Anwendung hinzuzufügen. Ihr Ziel ist es, eine kluge, unvoreingenommene und zukunftsorientierte Entscheidung zu treffen, die die Erfolgschancen Ihrer App maximiert. Als Schöpfer von next-firebase-auth-edge muss ich zugeben, dass es nicht meine Stärke ist, eine völlig unvoreingenommene Meinung abzugeben, aber ich werde zumindest versuchen, den Ansatz zu rechtfertigen, den ich beim Entwurf der Bibliothek gewählt habe. Wir hoffen, dass Sie am Ende dieses Leitfadens feststellen werden, dass der Ansatz sowohl einfach als auch langfristig praktikabel ist.
Ich erspare Ihnen lange Einführungen. Lassen Sie mich nur sagen, dass die Idee für die Bibliothek von einer Situation inspiriert wurde, die möglicherweise der Ihren ähnelt. Es war die Zeit, als Next.js eine Canary-Version von App Router veröffentlichte. Ich habe an einer App gearbeitet, die stark auf Umschreibungen und interne Weiterleitungen angewiesen war. Dafür verwendeten wir den benutzerdefinierten Node.js express
-Server, der die Next.js-App rendert.
Wir waren von den App Router- und Serverkomponenten sehr begeistert, waren uns jedoch bewusst, dass sie nicht mit unserem benutzerdefinierten Server kompatibel sein würden. Middleware schien eine leistungsstarke Funktion zu sein, die wir nutzen konnten, um die Notwendigkeit eines benutzerdefinierten Express-Servers zu eliminieren und uns stattdessen ausschließlich auf die integrierten Funktionen von Next.js zu verlassen, um Benutzer dynamisch auf verschiedene Seiten umzuleiten und umzuschreiben.
Dieses Mal verwendeten wir next-firebase-auth . Die Bibliothek hat uns sehr gut gefallen, aber sie hat unsere Authentifizierungslogik über next.config.js
, pages/_app.tsx
, pages/api/login.ts
und pages/api/logout.ts
verbreitet, die als Legacy gelten würden früh genug. Außerdem war die Bibliothek nicht mit der Middleware kompatibel, was uns daran hinderte, URLs neu zu schreiben oder Benutzer basierend auf ihrem Kontext umzuleiten.
Also begann ich mit der Suche, fand aber zu meiner Überraschung keine Bibliothek, die die Firebase-Authentifizierung innerhalb der Middleware unterstützte. – Warum könnte das sein? Es ist unmöglich! Als Softwareentwickler mit mehr als 11 Jahren kommerzieller Erfahrung in Node.js und React bereitete ich mich darauf vor, dieses Rätsel anzugehen.
Also fing ich an. Und die Antwort wurde offensichtlich. Middleware wird in Edge Runtime ausgeführt. In Edge Runtime ist keine Firebase-Bibliothek verfügbar, die mit Web-Crypto-APIs kompatibel ist. Ich war dem Untergang geweiht . Ich fühlte mich hilflos. Ist dies das erste Mal, dass ich tatsächlich warten muss, um mit den neuen und ausgefallenen APIs spielen zu können? - Nein. Ein bewachter Topf kocht niemals ueber. Ich hörte schnell auf zu schluchzen und begann , next-firebase-auth , firebase-admin und mehrere andere JWT-Authentifizierungsbibliotheken zurückzuentwickeln und sie an die Edge Runtime anzupassen. Ich nutzte die Gelegenheit, um alle Probleme anzugehen, auf die ich bei früheren Authentifizierungsbibliotheken gestoßen war, mit dem Ziel, die leichteste, am einfachsten zu konfigurierende und zukunftsorientierte Authentifizierungsbibliothek zu erstellen.
Etwa zwei Wochen später wurde Version 0.0.1
von next-firebase-auth-edge geboren. Es war ein solider Proof of Concept, aber Sie würden Version 0.0.1
nicht verwenden wollen. Vertrau mir.
Fast zwei Jahre später freue ich mich, Ihnen mitteilen zu können, dass die Bibliothek nach 372 Commits , 110 gelösten Problemen und einer Menge unschätzbarem Feedback von großartigen Entwicklern aus aller Welt ein Stadium erreicht hat, in dem meine anderen Knoten mir zustimmend zustimmen.
In dieser Anleitung werde ich Version 1.4.1 von next-firebase-auth-edge verwenden, um eine authentifizierte Next.js-App von Grund auf zu erstellen. Wir werden jeden Schritt im Detail durchgehen, beginnend mit der Erstellung eines neuen Firebase-Projekts und einer Next.js-App, gefolgt von der Integration mit den Bibliotheken next-firebase-auth-edge
und firebase/auth
. Am Ende dieses Tutorials werden wir die App auf Vercel bereitstellen, um zu bestätigen, dass alles sowohl lokal als auch in der produktionsbereiten Umgebung funktioniert.
In diesem Teil wird davon ausgegangen, dass Sie die Firebase-Authentifizierung noch nicht eingerichtet haben. Andernfalls können Sie gerne mit dem nächsten Teil fortfahren.
Gehen wir zur Firebase Console und erstellen ein Projekt
Nachdem das Projekt erstellt wurde, aktivieren wir die Firebase-Authentifizierung. Öffnen Sie die Konsole, gehen Sie zu Erstellen > Authentifizierung > Anmeldemethode und aktivieren Sie die E-Mail- und Kennwortmethode . Das ist die Methode, die wir in unserer App unterstützen werden
Nachdem Sie Ihre erste Anmeldemethode aktiviert haben, sollte die Firebase-Authentifizierung für Ihr Projekt aktiviert sein und Sie können Ihren Web-API-Schlüssel in den Projekteinstellungen abrufen
Kopieren Sie den API-Schlüssel und bewahren Sie ihn sicher auf. Öffnen wir nun die nächste Registerkarte – Cloud Messaging – und notieren Sie die Absender-ID . Wir werden es später brauchen.
Zu guter Letzt müssen wir Anmeldeinformationen für das Dienstkonto generieren. Dadurch erhält Ihre App vollen Zugriff auf Ihre Firebase-Dienste. Gehen Sie zu Projekteinstellungen > Dienstkonten und klicken Sie auf Neuen privaten Schlüssel generieren . Dadurch wird eine .json
Datei mit den Anmeldeinformationen des Dienstkontos heruntergeladen. Speichern Sie diese Datei an einem bekannten Ort.
Das ist es! Wir sind bereit, die Next.js-App mit der Firebase-Authentifizierung zu integrieren
In dieser Anleitung wird davon ausgegangen, dass Sie Node.js und npm installiert haben. Die in diesem Tutorial verwendeten Befehle wurden anhand der neuesten LTS Node.js v20 überprüft. Sie können die Knotenversion überprüfen, indem Sie node -v
im Terminal ausführen. Sie können auch Tools wie NVM verwenden, um schnell zwischen Node.js-Versionen zu wechseln.
Öffnen Sie Ihr Lieblingsterminal, navigieren Sie zu Ihrem Projektordner und führen Sie es aus
npx create-next-app@latest
Um es einfach zu halten, verwenden wir die Standardkonfiguration. Das bedeutet, dass wir TypeScript
und tailwind
verwenden werden
✔ 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
Navigieren wir zum Stammverzeichnis des Projekts und stellen wir sicher, dass alle Abhängigkeiten installiert sind
cd my-app npm install
Um zu bestätigen, dass alles wie erwartet funktioniert, starten wir den Next.js-Entwicklungsserver mit dem Befehl npm run dev
. Wenn Sie http://localhost:3000 öffnen, sollte die Begrüßungsseite von Next.j angezeigt werden, die etwa so aussieht:
Bevor wir mit der Integration in Firebase beginnen, benötigen wir eine sichere Möglichkeit zum Speichern und Lesen unserer Firebase-Konfiguration. Glücklicherweise wird Next.js mit integrierter Dotenv- Unterstützung ausgeliefert.
Öffnen Sie Ihren bevorzugten Code-Editor und navigieren Sie zum Projektordner
Erstellen wir die Datei .env.local
im Stammverzeichnis des Projekts und füllen sie mit den folgenden Umgebungsvariablen:
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=...
Bitte beachten Sie, dass Variablen mit dem Präfix NEXT_PUBLIC_
im clientseitigen Bundle verfügbar sind. Wir benötigen diese, um das Firebase Auth Client SDK einzurichten
NEXT_PUBLIC_FIREBASE_PROJECT_ID
, FIREBASE_ADMIN_CLIENT_EMAIL
und FIREBASE_ADMIN_PRIVATE_KEY
können aus .json
Datei abgerufen werden, die nach dem Generieren der Anmeldeinformationen für das Dienstkonto heruntergeladen wurde
AUTH_COOKIE_NAME
ist der Name des Cookies, das zum Speichern von Benutzeranmeldeinformationen verwendet wird
AUTH_COOKIE_SIGNATURE_KEY_CURRENT
und AUTH_COOKIE_SIGNATURE_KEY_PREVIOUS
sind Geheimnisse, mit denen wir die Anmeldeinformationen signieren werden
NEXT_PUBLIC_FIREBASE_API_KEY
ist ein Web-API-Schlüssel , der von der allgemeinen Seite „Projekteinstellungen“ abgerufen wird
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN
ist Ihre Projekt-ID .firebaseapp.com
NEXT_PUBLIC_FIREBASE_DATABASE_URL
ist Ihre Projekt-ID .firebaseio.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID
kann auf der Seite „Projekteinstellungen“ > „Cloud Messaging“ abgerufen werden
USE_SECURE_COOKIES
werden nicht für die lokale Entwicklung verwendet, sind aber nützlich, wenn wir unsere App in Vercel bereitstellen
next-firebase-auth-edge
und Erstkonfiguration Fügen Sie die Bibliothek zu den Abhängigkeiten des Projekts hinzu, indem Sie npm install next-firebase-auth-edge@^1.4.1
ausführen
Lassen Sie uns die Datei config.ts
erstellen, um unsere Projektkonfiguration zu kapseln. Dies ist nicht erforderlich, erleichtert aber die Lesbarkeit der Codebeispiele.
Verbringen Sie nicht zu viel Zeit damit, über diese Werte nachzudenken. Wir werden sie im weiteren Verlauf detaillierter erläutern.
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 };
Erstellen Sie die Datei middleware.ts
im Stammverzeichnis des Projekts und fügen Sie Folgendes ein
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", ], };
Ob Sie es glauben oder nicht, wir haben gerade den Server unserer App in die Firebase-Authentifizierung integriert. Bevor wir es tatsächlich verwenden, erklären wir die Konfiguration ein wenig:
loginPath
weist authMiddleware an, den Endpunkt GET /api/login
verfügbar zu machen. Wenn dieser Endpunkt mit dem Header Authorization: Bearer ${idToken}
*“ aufgerufen wird, antwortet er mit dem Header „HTTP(S)-Only Set-Cookie
, der signierte benutzerdefinierte Token und Aktualisierungstoken enthält
* idToken
wird mit der Funktion getIdToken
abgerufen, die im Firebase Client SDK verfügbar ist. Mehr dazu später.
In ähnlicher Weise weist logoutPath
die Middleware an, GET /api/logout
verfügbar zu machen, erfordert jedoch keine zusätzlichen Header. Beim Aufruf werden Authentifizierungscookies aus dem Browser entfernt.
apiKey
ist ein Web-API-Schlüssel. Middleware verwendet es, um benutzerdefinierte Token zu aktualisieren und Authentifizierungscookies zurückzusetzen, nachdem die Anmeldeinformationen abgelaufen sind.
cookieName
ist der Name des Cookies, das von den Endpunkten /api/login
und /api/logout
gesetzt und entfernt wird
cookieSignatureKeys
ist eine Liste geheimer Schlüssel, mit denen Benutzeranmeldeinformationen signiert werden. Anmeldeinformationen werden immer mit dem ersten Schlüssel in der Liste signiert, daher müssen Sie mindestens einen Wert angeben. Sie können mehrere Schlüssel bereitstellen, um eine Schlüsselrotation durchzuführen
cookieSerializeOptions
sind Optionen, die beim Generieren Set-Cookie
Headers an Cookie übergeben werden. Weitere Informationen finden Sie in der Cookie- README-Datei
serviceAccount
autorisiert die Bibliothek, Ihre Firebase-Dienste zu nutzen.
Der Matcher weist den Next.js-Server an, Middleware für /api/login
, /api/logout
, /
und jeden anderen Pfad auszuführen, bei dem es sich nicht um eine Datei oder einen API-Aufruf handelt.
export const config = { matcher: [ "/", "/((?!_next|api|.*\\.).*)", "/api/login", "/api/logout", ], };
Sie fragen sich vielleicht, warum wir Middleware nicht für alle /api/*
-Aufrufe aktivieren. Das wäre möglich, aber es empfiehlt sich, nicht authentifizierte Aufrufe innerhalb des API-Route-Handlers selbst zu verarbeiten. Dies würde den Rahmen dieses Tutorials etwas sprengen, aber wenn Sie interessiert sind, lassen Sie es mich wissen und ich bereite einige Beispiele vor!
Wie Sie sehen, ist die Konfiguration minimal und hat einen klar definierten Zweck. Beginnen wir nun mit dem Aufruf unserer Endpunkte /api/login
und /api/logout
.
Um die Sache so einfach wie möglich zu gestalten, löschen wir die Standard-Startseite von Next.js und ersetzen sie durch einige personalisierte Inhalte
Öffnen Sie ./app/page.tsx
und fügen Sie Folgendes ein:
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> ); }
Lassen Sie uns das Stück für Stück aufschlüsseln.
getTokens
Funktion dient zum Validieren und Extrahieren von Benutzeranmeldeinformationen aus Cookies
const tokens = await getTokens(cookies(), { apiKey: clientConfig.apiKey, cookieName: serverConfig.cookieName, cookieSignatureKeys: serverConfig.cookieSignatureKeys, serviceAccount: serverConfig.serviceAccount, });
Es wird mit null
aufgelöst, wenn der Benutzer nicht authentifiziert ist, oder ein Objekt mit zwei Eigenschaften:
token
, bei dem es sich um eine idToken- string
handelt, mit der Sie API-Anfragen an externe Back-End-Dienste autorisieren können. Dies liegt etwas außerhalb des Rahmens, aber es ist erwähnenswert, dass die Bibliothek eine verteilte Servicearchitektur ermöglicht. Der token
ist mit allen offiziellen Firebase-Bibliotheken auf allen Plattformen kompatibel und einsatzbereit.
decodedToken
ist, wie der Name schon sagt, eine entschlüsselte Version des token
, die alle zur Identifizierung des Benutzers erforderlichen Informationen enthält, einschließlich E-Mail-Adresse, Profilbild und benutzerdefinierten Ansprüchen , was uns außerdem ermöglicht, den Zugriff basierend auf Rollen und Berechtigungen einzuschränken.
Nachdem wir tokens
erhalten haben, verwenden wir die Funktion „notFound“ von next/navigation
, um sicherzustellen, dass die Seite nur für authentifizierte Benutzer zugänglich ist
if (!tokens) { notFound(); }
Abschließend rendern wir einige grundlegende, personalisierte Benutzerinhalte
<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>
Lassen wir es laufen.
Falls Sie Ihren Entwicklungsserver geschlossen haben, führen Sie einfach npm run dev
aus.
Wenn Sie versuchen, auf http://localhost:3000/ zuzugreifen, sollte 404 angezeigt werden: Diese Seite konnte nicht gefunden werden.
Erfolg! Wir haben unsere Geheimnisse vor neugierigen Blicken geschützt!
firebase
installieren und Firebase Client SDK initialisieren Führen Sie npm install firebase
im Stammverzeichnis des Projekts aus
Erstellen Sie nach der Installation des Client-SDK die Datei firebase.ts
im Stammverzeichnis des Projekts und fügen Sie Folgendes ein
import { initializeApp } from 'firebase/app'; import { clientConfig } from './config'; export const app = initializeApp(clientConfig);
Dadurch wird das Firebase Client SDK initialisiert und das App-Objekt für Client-Komponenten verfügbar gemacht
Was nützt eine supersichere Homepage, wenn niemand sie sehen kann? Lassen Sie uns eine einfache Registrierungsseite erstellen, um Menschen Zugang zu unserer App zu ermöglichen.
Lasst uns eine neue, schicke Seite unter ./app/register/page.tsx
erstellen
"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> ); }
Ich weiß. Es ist viel Text, aber haben Sie Geduld.
Wir beginnen mit "use client";
um anzugeben, dass die Registrierungsseite clientseitige APIs verwendet
const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmation, setConfirmation] = useState(""); const [error, setError] = useState("");
Dann definieren wir einige Variablen und Setter, um unseren Formularstatus zu halten
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); } }
Hier definieren wir unsere Formularübermittlungslogik. Zuerst überprüfen wir, ob password
und confirmation
gleich sind, andernfalls aktualisieren wir den Fehlerstatus. Wenn die Werte gültig sind, erstellen wir ein Benutzerkonto mit createUserWithEmailAndPassword
aus firebase/auth
. Wenn dieser Schritt fehlschlägt (z. B. weil die E-Mail angenommen wurde), informieren wir den Benutzer, indem wir den Fehler aktualisieren.
Wenn alles gut geht, leiten wir den Benutzer zur Seite /login
weiter. Wahrscheinlich sind Sie im Moment verwirrt, und Sie haben Recht. /login
Seite existiert noch nicht. Wir bereiten uns gerade auf das vor, was als nächstes kommt.
Wenn Sie http://localhost:3000/register besuchen, sollte die Seite ungefähr so aussehen:
Da sich Benutzer nun registrieren können, können sie ihre Identität nachweisen
Erstellen Sie eine Anmeldeseite unter ./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> ); }
Wie Sie sehen, ist es der Registrierungsseite ziemlich ähnlich. Konzentrieren wir uns auf das Entscheidende:
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); } }
Hier geschieht die ganze Magie . Wir verwenden signInEmailAndPassword
von firebase/auth
um idToken
des Benutzers abzurufen.
Dann rufen wir den von der Middleware bereitgestellten /api/login
Endpunkt auf. Dieser Endpunkt aktualisiert unsere Browser-Cookies mit Benutzeranmeldeinformationen.
Schließlich leiten wir den Benutzer auf die Startseite um, indem wir router.push("/");
aufrufen.
Die Anmeldeseite sollte ungefähr so aussehen
Lass es uns testen!
Gehen Sie zu http://localhost:3000/register und geben Sie eine zufällige E-Mail-Adresse und ein Passwort ein, um ein Konto zu erstellen. Verwenden Sie diese Anmeldeinformationen auf der Seite http://localhost:3000/login . Nachdem Sie auf die Eingabetaste geklickt haben, sollten Sie zur supersicheren Startseite weitergeleitet werden
Endlich müssen wir unsere eigene , persönliche , extrem sichere Homepage sehen! Aber warte! Wie kommen wir raus?
Wir müssen eine Abmeldeschaltfläche hinzufügen, um uns nicht für immer (oder 12 Tage) von der Welt auszuschließen.
Bevor wir beginnen, müssen wir eine Client-Komponente erstellen, die uns mithilfe des Firebase Client SDK abmelden kann.
Erstellen wir eine neue Datei unter ./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> ); }
Wie Sie vielleicht bemerkt haben, handelt es sich hierbei um eine leicht modifizierte Version unserer ./app/page.tsx
. Wir mussten eine separate Client-Komponente erstellen, da getTokens
nur innerhalb von Serverkomponenten und API-Routenhandlern funktioniert, während signOut
und useRouter
im Client-Kontext ausgeführt werden müssen. Ein bisschen kompliziert, ich weiß, aber es ist tatsächlich ziemlich mächtig. Ich erkläre es später.
Konzentrieren wir uns auf den Abmeldevorgang
const router = useRouter(); async function handleLogout() { await signOut(getAuth(app)); await fetch("/api/logout"); router.push("/login"); }
Zuerst melden wir uns vom Firebase Client SDK ab. Dann rufen wir den von der Middleware bereitgestellten /api/logout
Endpunkt auf. Zum Abschluss leiten wir den Benutzer zur Seite /login
weiter.
Lassen Sie uns unsere Server-Homepage aktualisieren. Gehen Sie zu ./app/page.tsx
und fügen Sie Folgendes ein
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} />; }
Jetzt ist unsere Home
Server-Komponente nur für das Abrufen von Benutzer-Tokens und deren Weitergabe an HomePage
Client-Komponente verantwortlich. Dies ist tatsächlich ein ziemlich häufiges und nützliches Muster.
Lass uns das testen:
Voila! Wir können uns nun nach Belieben bei der Anwendung an- und abmelden. Das ist großartig!
Oder ist es?
Wenn ein nicht authentifizierter Benutzer versucht, die Startseite durch Öffnen von http://localhost:3000/ aufzurufen, wird 404 angezeigt: Diese Seite konnte nicht gefunden werden.
Außerdem können authentifizierte Benutzer weiterhin auf die Seiten http://localhost:3000/register und http://localhost:3000/login zugreifen, ohne sich abmelden zu müssen.
Wir können es besser machen.
Es scheint, dass wir eine Umleitungslogik hinzufügen müssen. Lassen Sie uns einige Regeln definieren:
/register
und /login
zuzugreifen, sollten wir ihn zu /
umleiten./
zuzugreifen, sollten wir ihn zu /login
umleiten
Middleware ist eine der besten Möglichkeiten, Weiterleitungen in Next.js-Apps zu verarbeiten. Glücklicherweise unterstützt authMiddleware
eine Reihe von Optionen und Hilfsfunktionen, um eine Vielzahl von Umleitungsszenarien zu bewältigen.
Öffnen wir die Datei middleware.ts
und fügen diese aktualisierte Version ein
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", ], };
Das sollte es sein. Wir haben alle Weiterleitungsregeln implementiert. Lassen Sie uns das aufschlüsseln.
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
wird aufgerufen, wenn gültige Benutzeranmeldeinformationen an die Anforderung angehängt sind, d. h. Der Benutzer ist authentifiziert. Es wird mit tokens
als erstem und den geänderten Anforderungsheadern als zweitem Argument aufgerufen. Es sollte mit NextResponse
aufgelöst werden.
redirectToHome
von next-firebase-auth-edge
ist eine Hilfsfunktion, die ein Objekt zurückgibt, das zu NextResponse.redirect(new URL(“/“))
vereinfacht werden kann.
Durch Überprüfen von PUBLIC_PATHS.includes(request.nextUrl.pathname)
überprüfen wir, ob der authentifizierte Benutzer versucht, auf die Seite /login
oder /register
zuzugreifen, und leiten in diesem Fall zur Startseite weiter.
handleInvalidToken: async (reason) => { console.info('Missing or malformed credentials', {reason}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); },
handleInvalidToken
wird aufgerufen, wenn etwas Erwartetes passiert. Eines dieser erwarteten Ereignisse ist, dass der Benutzer Ihre App zum ersten Mal sieht, von einem anderen Gerät aus oder nachdem die Anmeldeinformationen abgelaufen sind.
Da wir wissen, dass handleInvalidToken
für nicht authentifizierte Benutzer aufgerufen wird, können wir mit der zweiten Regel fortfahren: Wenn nicht authentifizierte Benutzer versuchen, auf die Seite /
zuzugreifen , sollten wir sie zu /login
umleiten
Da keine andere Bedingung erfüllt werden muss, geben wir einfach das Ergebnis von redirectToLogin
zurück, das zu NextResponse.redirect(new URL(“/login”))
vereinfacht werden kann. Es stellt außerdem sicher, dass der Benutzer nicht in eine Umleitungsschleife gerät.
Zuletzt,
handleError: async (error) => { console.error('Unhandled authentication error', {error}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); }
Im Gegensatz zu handleInvalidToken
wird handleError
aufgerufen, wenn etwas Unerwartetes* passiert und möglicherweise untersucht werden muss. Eine Liste möglicher Fehler mit Beschreibung finden Sie in der Dokumentation
Im Fehlerfall protokollieren wir dies und leiten den Benutzer sicher zur Anmeldeseite weiter
* handleError
kann mit dem Fehler INVALID_ARGUMENT
aufgerufen werden, nachdem die öffentlichen Schlüssel von Google aktualisiert wurden.
Dies ist eine Form der Schlüsselrotation und wird erwartet . Weitere Informationen finden Sie in dieser Github-Ausgabe
Nun, das ist es. Endlich.
Melden wir uns von unserer Web-App ab und öffnen Sie http://localhost:3000/ . Wir sollten zur Seite /login
weitergeleitet werden.
Melden wir uns erneut an und versuchen, http://localhost:3000/login einzugeben. Wir sollten zur Seite /
weitergeleitet werden.
Wir haben nicht nur für ein nahtloses Benutzererlebnis gesorgt. next-firebase-auth-edge
ist eine Bibliothek mit Null-Bundle-Größe , die nur auf dem Server der App funktioniert und keinen zusätzlichen clientseitigen Code einführt. Das resultierende Paket ist wirklich minimal . Das nenne ich perfekt.
Unsere App ist jetzt sowohl in den Server- als auch in den Clientkomponenten vollständig in die Firebase-Authentifizierung integriert. Wir sind bereit, das volle Potenzial von Next.js auszuschöpfen!
Den Quellcode der App finden Sie unter next-firebase-auth-edge/examples/next-typescript-minimal
In diesem Leitfaden haben wir die Integration der neuen Next.js-App mit der Firebase-Authentifizierung beschrieben.
Obwohl der Artikel recht umfangreich ist, wurden einige wichtige Teile des Authentifizierungsablaufs weggelassen, etwa das Formular zum Zurücksetzen des Passworts oder andere Anmeldemethoden als E-Mail und Passwort.
Wenn Sie an der Bibliothek interessiert sind, können Sie sich eine Vorschau der vollständigen Next-Firebase-Auth-Edge-Starter-Demoseite ansehen.
Es bietet Firestore-Integration , Serveraktionen , App-Check-Unterstützung und mehr
Die Bibliothek bietet eine eigene Dokumentationsseite mit unzähligen Beispielen
Wenn Ihnen der Artikel gefallen hat, würde ich mich freuen, wenn Sie das Next-Firebase-Auth-Edge- Repository erwähnen. Prost! 🎉
In diesem Bonusleitfaden erfahren Sie, wie Sie Ihre Next.js-App in Vercel bereitstellen
Um die Bereitstellung auf Vercel durchführen zu können, müssen Sie ein Repository für Ihre neue App erstellen.
Gehen Sie zu https://github.com/ und erstellen Sie ein neues Repository.
create-next-app
hat bereits ein lokales Git-Repository für uns initiiert, Sie müssen also nur zum Stammordner Ihres Projekts gehen und Folgendes ausführen:
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
Gehen Sie zu https://vercel.com/ und melden Sie sich mit Ihrem Github-Konto an
Nachdem Sie sich angemeldet haben, gehen Sie zur Übersichtsseite von Vercel und klicken Sie auf Neu hinzufügen > Projekt
Klicken Sie neben dem Github-Repository, das wir gerade erstellt haben, auf Importieren . Noch nicht bereitstellen.
Vor der Bereitstellung müssen wir die Projektkonfiguration bereitstellen. Fügen wir einige Umgebungsvariablen hinzu:
Denken Sie daran, USE_SECURE_COOKIES
auf true
zu setzen, da Vercel standardmäßig HTTPS verwendet
Jetzt können wir auf „Bereitstellen“ klicken
Warten Sie ein oder zwei Minuten und Sie sollten mit einer URL wie dieser auf Ihre App zugreifen können: https://next-typescript-minimal-xi.vercel.app/
Erledigt. Ich wette, Sie haben nicht damit gerechnet, dass es so einfach sein würde.
Wenn Ihnen der Leitfaden gefallen hat, würde ich mich freuen, wenn Sie das Next-Firebase-Auth-Edge- Repository erwähnen.
Gerne können Sie mir auch Ihr Feedback in den Kommentaren mitteilen. Prost! 🎉