next-firebase-auth-edge
Vous avez probablement trouvé cet article en cherchant des moyens d'ajouter l'authentification Firebase à votre application Next.js existante ou nouvelle. Votre objectif est de prendre une décision intelligente, impartiale et tournée vers l’avenir qui maximisera les chances de succès de votre application. En tant que créateur de next-firebase-auth-edge, je dois admettre que donner une opinion totalement impartiale n'est pas mon fort, mais au moins j'essaierai de justifier l'approche que j'ai adoptée lors de la conception de la bibliothèque. Espérons qu’à la fin de ce guide, vous trouverez l’approche à la fois simple et viable à long terme.
Je vous épargnerai de longues présentations. Permettez-moi simplement de dire que l'idée de la bibliothèque a été inspirée par une situation peut-être similaire à la vôtre. C'était l'époque où Next.js publiait une version canari d' App Router . Je travaillais sur une application qui reposait fortement sur des réécritures et des redirections internes. Pour cela, nous utilisions l'application Next.js de rendu du serveur express
Node.js personnalisé.
Nous étions vraiment enthousiasmés par App Router et Server Components , tout en étant conscients qu'ils ne seraient pas compatibles avec notre serveur personnalisé. Le middleware semblait être une fonctionnalité puissante que nous pourrions exploiter pour éliminer le besoin d'un serveur Express personnalisé, en choisissant plutôt de s'appuyer uniquement sur les fonctionnalités intégrées de Next.js pour rediriger et réécrire les utilisateurs vers différentes pages de manière dynamique.
Cette fois-là, nous utilisions next-firebase-auth . Nous avons vraiment aimé la bibliothèque, mais elle a étendu notre logique d'authentification à travers les fichiers next.config.js
, pages/_app.tsx
, pages/api/login.ts
, pages/api/logout.ts
, qui allaient être considérés comme hérités assez tôt. De plus, la bibliothèque n'était pas compatible avec le middleware, ce qui nous empêchait de réécrire les URL ou de rediriger les utilisateurs en fonction de leur contexte.
J'ai donc commencé ma recherche, mais à ma grande surprise, je n'ai trouvé aucune bibliothèque prenant en charge l'authentification Firebase dans le middleware. – Pourquoi est-ce possible ? C'est impossible! En tant qu'ingénieur logiciel avec plus de 11 ans d'expérience commerciale dans Node.js et React, je me préparais à résoudre cette énigme.
Alors, j'ai commencé. Et la réponse est devenue évidente. Le middleware s’exécute dans Edge Runtime . Il n'existe pas de bibliothèque Firebase compatible avec les API Web Crypto disponibles dans Edge Runtime . J'étais condamné . Je me sentais impuissant. Est-ce la première fois que je devrai attendre pour pouvoir jouer avec les nouvelles API sophistiquées ? - Non. Tout vient à point à qui sait attendre. J'ai rapidement arrêté de sangloter et j'ai commencé à faire de l'ingénierie inverse next-firebase-auth , firebase-admin et plusieurs autres bibliothèques d'authentification JWT, en les adaptant au Edge Runtime. J'ai saisi l'opportunité de résoudre tous les problèmes que j'avais rencontrés avec les bibliothèques d'authentification précédentes, dans le but de créer la bibliothèque d'authentification la plus légère, la plus simple à configurer et tournée vers l'avenir.
Environ deux semaines plus tard, la version 0.0.1
de next-firebase-auth-edge était née. C'était une solide preuve de concept, mais vous ne voudriez pas utiliser la version 0.0.1
. Fais-moi confiance.
Près de deux ans plus tard , je suis ravi d'annoncer qu'après 372 commits , 110 problèmes résolus et une multitude de commentaires inestimables de développeurs formidables du monde entier, la bibliothèque a atteint un stade où mes autres nœuds me donnent leur approbation.
Dans ce guide, j'utiliserai la version 1.4.1 de next-firebase-auth-edge pour créer une application Next.js authentifiée à partir de zéro. Nous passerons en revue chaque étape en détail, en commençant par la création d'un nouveau projet Firebase et de l'application Next.js, suivie de l'intégration avec les bibliothèques next-firebase-auth-edge
et firebase/auth
. À la fin de ce didacticiel, nous déploierons l'application sur Vercel pour confirmer que tout fonctionne à la fois localement et dans un environnement prêt pour la production.
Cette partie suppose que vous n'avez pas encore configuré l'authentification Firebase. N'hésitez pas à passer à la partie suivante si c'est le cas.
Passons à la console Firebase et créons un projet
Une fois le projet créé, activons l'authentification Firebase. Ouvrez la console et suivez jusqu'à Créer > Authentification > Méthode de connexion et activez la méthode par e-mail et mot de passe . C'est la méthode que nous allons prendre en charge dans notre application
Après avoir activé votre première méthode de connexion, l'authentification Firebase doit être activée pour votre projet et vous pouvez récupérer votre clé API Web dans les paramètres du projet.
Copiez la clé API et conservez-la en sécurité. Maintenant, ouvrons l'onglet suivant – Cloud Messaging, et notons l'ID de l'expéditeur . Nous en aurons besoin plus tard.
Enfin et surtout, nous devons générer les informations d'identification du compte de service. Ceux-ci permettront à votre application d'obtenir un accès complet à vos services Firebase. Accédez à Paramètres du projet > Comptes de service et cliquez sur Générer une nouvelle clé privée . Cela téléchargera un fichier .json
avec les informations d'identification du compte de service. Enregistrez ce fichier dans un emplacement connu.
C'est ça! Nous sommes prêts à intégrer l'application Next.js avec l'authentification Firebase
Ce guide suppose que Node.js et npm sont installés. Les commandes utilisées dans ce didacticiel ont été vérifiées par rapport à la dernière version de LTS Node.js v20 . Vous pouvez vérifier la version du nœud en exécutant node -v
dans le terminal. Vous pouvez également utiliser des outils tels que NVM pour basculer rapidement entre les versions de Node.js.
Ouvrez votre terminal préféré, accédez à votre dossier de projets et exécutez
npx create-next-app@latest
Pour faire simple, utilisons la configuration par défaut. Cela signifie que nous utiliserons TypeScript
et 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
Naviguons vers le répertoire racine du projet et assurons-nous que toutes les dépendances sont installées
cd my-app npm install
Pour confirmer que tout fonctionne comme prévu, démarrons le serveur de développement Next.js avec la commande npm run dev
. Lorsque vous ouvrez http://localhost:3000 , vous devriez voir la page d'accueil de Next.js, semblable à celle-ci :
Avant de commencer l'intégration avec Firebase, nous avons besoin d'un moyen sécurisé pour stocker et lire notre configuration Firebase. Heureusement, Next.js est livré avec le support dotenv intégré.
Ouvrez votre éditeur de code préféré et accédez au dossier du projet
Créons le fichier .env.local
dans le répertoire racine du projet et remplissons-le avec les variables d'environnement suivantes :
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=...
Veuillez noter que les variables préfixées par NEXT_PUBLIC_
seront disponibles dans le bundle côté client. Nous en aurons besoin pour configurer le SDK Firebase Auth Client
NEXT_PUBLIC_FIREBASE_PROJECT_ID
, FIREBASE_ADMIN_CLIENT_EMAIL
et FIREBASE_ADMIN_PRIVATE_KEY
peuvent être récupérés à partir du fichier .json
téléchargé après avoir généré les informations d'identification du compte de service
AUTH_COOKIE_NAME
sera le nom du cookie utilisé pour stocker les informations d'identification de l'utilisateur
AUTH_COOKIE_SIGNATURE_KEY_CURRENT
et AUTH_COOKIE_SIGNATURE_KEY_PREVIOUS
sont des secrets avec lesquels nous allons signer les informations d'identification
NEXT_PUBLIC_FIREBASE_API_KEY
est la clé API Web récupérée de la page générale des paramètres du projet.
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN
est votre identifiant de projet .firebaseapp.com
NEXT_PUBLIC_FIREBASE_DATABASE_URL
est votre identifiant de projet .firebaseio.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID
peut être obtenu à partir de la page Paramètres du projet > Messagerie cloud .
USE_SECURE_COOKIES
ne sera pas utilisé pour le développement local, mais sera utile lorsque nous déploierons notre application sur Vercel
next-firebase-auth-edge
et configuration initiale Ajoutez la bibliothèque aux dépendances du projet en exécutant npm install next-firebase-auth-edge@^1.4.1
Créons le fichier config.ts
pour encapsuler la configuration de notre projet. Ce n'est pas obligatoire, mais cela rendra les exemples de code plus lisibles.
Ne passez pas trop de temps à réfléchir à ces valeurs. Nous les expliquerons plus en détail au fur et à mesure.
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 };
Créez le fichier middleware.ts
à la racine du projet et collez ce qui suit
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", ], };
Croyez-le ou non, mais nous venons d'intégrer le serveur de notre application avec l'authentification Firebase. Avant de l'utiliser réellement, expliquons un peu la configuration :
loginPath
demandera à authMiddleware d'exposer le point de terminaison GET /api/login
. Lorsque ce point de terminaison est appelé avec l'en-tête Authorization: Bearer ${idToken}
*, il répond avec l'en-tête HTTP(S)-Only Set-Cookie
contenant des jetons personnalisés et d'actualisation signés.
* idToken
est récupéré avec la fonction getIdToken
disponible dans le SDK client Firebase . Nous en reparlerons plus tard.
De même, logoutPath
demande au middleware d'exposer GET /api/logout
, mais il ne nécessite aucun en-tête supplémentaire. Lorsqu'il est appelé, il supprime les cookies d'authentification du navigateur.
apiKey
est une clé API Web. Le middleware l'utilise pour actualiser le jeton personnalisé et réinitialiser les cookies d'authentification après l'expiration des informations d'identification.
cookieName
est le nom du cookie défini et supprimé par les points de terminaison /api/login
et /api/logout
cookieSignatureKeys
une liste de clés secrètes avec lesquelles les informations d'identification des utilisateurs sont signées. Les informations d'identification seront toujours signées avec la première clé de la liste, vous devez donc fournir au moins une valeur. Vous pouvez fournir plusieurs clés pour effectuer une rotation de clé
cookieSerializeOptions
sont des options transmises au cookie lors de la génération de l'en-tête Set-Cookie
. Voir le cookie README pour plus d'informations
serviceAccount
autorise la bibliothèque à utiliser vos services Firebase.
Le matcher demande au serveur Next.js d'exécuter le Middleware sur /api/login
, /api/logout
et /
et tout autre chemin qui n'est pas un fichier ou un appel d'API.
export const config = { matcher: [ "/", "/((?!_next|api|.*\\.).*)", "/api/login", "/api/logout", ], };
Vous vous demandez peut-être pourquoi nous n'activons pas le middleware pour tous les appels /api/*
. Nous pourrions, mais c'est une bonne pratique de gérer les appels non authentifiés dans le gestionnaire de route API lui-même. C'est un peu hors du cadre de ce tutoriel, mais si vous êtes intéressé, faites-le-moi savoir et je vous préparerai quelques exemples !
Comme vous pouvez le constater, la configuration est minimale et avec un objectif clairement défini. Maintenant, commençons à appeler nos points de terminaison /api/login
et /api/logout
.
Pour rendre les choses aussi simples que possible, effaçons la page d'accueil par défaut de Next.js et remplaçons-la par du contenu personnalisé
Ouvrez ./app/page.tsx
et collez ceci :
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> ); }
Décomposons cela petit à petit.
La fonction getTokens
est conçue pour valider et extraire les informations d'identification des utilisateurs à partir des cookies
const tokens = await getTokens(cookies(), { apiKey: clientConfig.apiKey, cookieName: serverConfig.cookieName, cookieSignatureKeys: serverConfig.cookieSignatureKeys, serviceAccount: serverConfig.serviceAccount, });
Il se résout avec null
, si l'utilisateur n'est pas authentifié ou un objet contenant deux propriétés :
token
qui est string
idToken que vous pouvez utiliser pour autoriser les requêtes API vers des services backend externes. C'est un peu hors de portée, mais il convient de mentionner que la bibliothèque permet une architecture de services distribués. Le token
est compatible et prêt à être utilisé avec toutes les bibliothèques Firebase officielles sur toutes les plateformes.
decodedToken
comme son nom l'indique, est une version décodée du token
, qui contient toutes les informations nécessaires pour identifier l'utilisateur, y compris l'adresse e-mail, la photo de profil et les revendications personnalisées , ce qui nous permet en outre de restreindre l'accès en fonction des rôles et des autorisations.
Après avoir obtenu tokens
, nous utilisons la fonction notFound de next/navigation
pour nous assurer que la page n'est accessible qu'aux utilisateurs authentifiés.
if (!tokens) { notFound(); }
Enfin, nous rendons du contenu utilisateur de base et personnalisé
<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>
Allons-y.
Si vous avez fermé votre serveur de développement, exécutez simplement npm run dev
.
Lorsque vous essayez d'accéder à http://localhost:3000/ , vous devriez voir 404 : Cette page est introuvable.
Succès! Nous avons gardé nos secrets à l'abri des regards indiscrets !
firebase
et initialiser le SDK client Firebase Exécutez npm install firebase
dans le répertoire racine du projet
Une fois le SDK client installé, créez le fichier firebase.ts
dans le répertoire racine du projet et collez ce qui suit
import { initializeApp } from 'firebase/app'; import { clientConfig } from './config'; export const app = initializeApp(clientConfig);
Cela initialisera le SDK client Firebase et exposera l'objet d'application pour les composants clients.
Quel est l’intérêt d’avoir une page d’accueil super sécurisée si personne ne peut la consulter ? Créons une page d'inscription simple pour permettre aux gens d'accéder à notre application.
Créons une nouvelle page sophistiquée sous ./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> ); }
Je sais. Cela fait beaucoup de texte, mais soyez indulgents avec moi.
Nous commençons par "use client";
pour indiquer que la page d'inscription utilisera des API côté client
const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmation, setConfirmation] = useState(""); const [error, setError] = useState("");
Ensuite, nous définissons quelques variables et setters pour conserver notre état de formulaire
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); } }
Ici, nous définissons notre logique de soumission de formulaire. Tout d'abord, nous validons si password
et confirmation
sont égaux, sinon nous mettons à jour l'état d'erreur. Si les valeurs sont valides, nous créons un compte utilisateur avec createUserWithEmailAndPassword
depuis firebase/auth
. Si cette étape échoue (par exemple, l'e-mail est pris), nous informons l'utilisateur en mettant à jour l'erreur.
Si tout se passe bien, nous redirigeons l'utilisateur vers la page /login
. Vous êtes probablement confus en ce moment, et vous avez raison de l’être. La page /login
n’existe pas encore. Nous préparons simplement la suite.
Lorsque vous visitez http://localhost:3000/register , la page devrait ressembler à peu près à ceci :
Maintenant que les utilisateurs peuvent s'inscrire, laissez-les prouver leur identité
Créez une page de connexion sous ./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> ); }
Comme vous le voyez, c'est assez similaire à la page d'inscription. Concentrons-nous sur le point crucial :
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); } }
C'est là que toute la magie opère. Nous utilisons signInEmailAndPassword
depuis firebase/auth
pour récupérer idToken
de l'utilisateur.
Ensuite, nous appelons le point de terminaison /api/login
exposé par le middleware. Ce point de terminaison met à jour les cookies de notre navigateur avec les informations d'identification de l'utilisateur.
Enfin, nous redirigeons l'utilisateur vers la page d'accueil en appelant router.push("/");
La page de connexion devrait ressembler à ceci
Testons-le !
Accédez à http://localhost:3000/register , entrez une adresse e-mail et un mot de passe aléatoires pour créer un compte. Utilisez ces informations d'identification sur la page http://localhost:3000/login . Après avoir cliqué sur Entrée , vous devriez être redirigé vers une page d'accueil super sécurisée
Il nous faut enfin voir notre propre page d'accueil, personnelle et ultra sécurisée ! Mais attendez! Comment on s'en sort ?
Nous devons ajouter un bouton de déconnexion pour ne pas nous exclure du monde pour toujours (ou 12 jours).
Avant de commencer, nous devons créer un composant client qui pourra nous déconnecter à l'aide du SDK client Firebase.
Créons un nouveau fichier sous ./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> ); }
Comme vous l'avez peut-être remarqué, il s'agit d'une version légèrement modifiée de notre ./app/page.tsx
. Nous avons dû créer un composant client distinct, car getTokens
ne fonctionne qu'à l'intérieur des composants serveur et des gestionnaires de routes API , tandis que signOut
et useRouter
doivent être exécutés dans un contexte client. Un peu compliqué, je sais, mais c'est en fait assez puissant. Je t'expliquerai plus tard.
Concentrons-nous sur le processus de déconnexion
const router = useRouter(); async function handleLogout() { await signOut(getAuth(app)); await fetch("/api/logout"); router.push("/login"); }
Tout d’abord, nous nous déconnectons du SDK client Firebase. Ensuite, nous appelons le point de terminaison /api/logout
exposé par le middleware. Nous terminons en redirigeant l'utilisateur vers la page /login
.
Mettons à jour la page d'accueil de notre serveur. Accédez à ./app/page.tsx
et collez ce qui suit
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} />; }
Désormais, notre composant serveur Home
est uniquement responsable de la récupération des jetons utilisateur et de leur transmission au composant client HomePage
. Il s’agit en fait d’un modèle assez courant et utile.
Testons ceci :
Voilà ! Nous pouvons désormais nous connecter et nous déconnecter de l'application à notre guise. C'est parfait!
Ou est-ce?
Lorsqu'un utilisateur non authentifié tente d'accéder à la page d'accueil en ouvrant http://localhost:3000/, nous affichons 404 : Cette page est introuvable.
De plus, les utilisateurs authentifiés peuvent toujours accéder aux pages http://localhost:3000/register et http://localhost:3000/login sans avoir à se déconnecter.
Nous pouvons faire mieux.
Il semble que nous devions ajouter une logique de redirection. Définissons quelques règles :
/register
et /login
, nous devons le rediriger vers /
/
, nous devons le rediriger vers /login
Le middleware est l'un des meilleurs moyens de gérer les redirections dans les applications Next.js. Heureusement, authMiddleware
prend en charge un certain nombre d'options et de fonctions d'assistance pour gérer un large éventail de scénarios de redirection.
Ouvrons le fichier middleware.ts
et collons cette version mise à jour
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", ], };
Ça devrait être ça. Nous avons implémenté toutes les règles de redirection. Décomposons cela.
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
est appelé lorsque des informations d'identification utilisateur valides sont jointes à la demande, c'est-à-dire. l'utilisateur est authentifié. Il est appelé avec l'objet tokens
comme premier et les en-têtes de requête modifiés comme deuxième argument. Cela devrait être résolu avec NextResponse
.
redirectToHome
de next-firebase-auth-edge
est une fonction d'assistance qui renvoie un objet qui peut être simplifié en NextResponse.redirect(new URL(“/“))
En vérifiant PUBLIC_PATHS.includes(request.nextUrl.pathname)
, nous validons si l'utilisateur authentifié tente d'accéder à la page /login
ou /register
, et redirigeons vers l'accueil si tel est le cas.
handleInvalidToken: async (reason) => { console.info('Missing or malformed credentials', {reason}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); },
handleInvalidToken
est appelé lorsque quelque chose attendu se produit. L'un de ces événements attendus est que l'utilisateur voit votre application pour la première fois, depuis un autre appareil ou après l'expiration des informations d'identification.
Sachant que handleInvalidToken
est appelé pour un utilisateur non authentifié, nous pouvons procéder avec la deuxième règle : lorsqu'un utilisateur non authentifié tente d'accéder à la page /
, nous devons le rediriger vers /login
Comme il n’y a aucune autre condition à remplir, nous renvoyons simplement le résultat de redirectToLogin
qui peut être simplifié en NextResponse.redirect(new URL(“/login”))
. Cela garantit également que l'utilisateur ne tombe pas dans une boucle de redirection.
Dernièrement,
handleError: async (error) => { console.error('Unhandled authentication error', {error}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); }
Contrairement à handleInvalidToken
, handleError
est appelé lorsque quelque chose d'inattendu* se produit et doit éventuellement faire l'objet d'une enquête. Vous pouvez trouver une liste des erreurs possibles avec leur description dans la documentation
En cas d'erreur, nous enregistrons ce fait et redirigeons l'utilisateur en toute sécurité vers la page de connexion
* handleError
peut être appelé avec l'erreur INVALID_ARGUMENT
après la mise à jour des clés publiques de Google.
Il s'agit d'une forme de rotation des clés et elle est attendue . Voir ce problème Github pour plus d'informations
Maintenant, c'est tout. Enfin.
Déconnectons-nous de notre application Web et ouvrons http://localhost:3000/ . Nous devrions être redirigés vers la page /login
.
Reconnectons-nous et essayons de saisir http://localhost:3000/login . Nous devrions être redirigés vers la page /
.
Non seulement nous avons fourni une expérience utilisateur transparente. next-firebase-auth-edge
est une bibliothèque de taille zéro qui fonctionne uniquement sur le serveur de l'application et n'introduit pas de code supplémentaire côté client. Le bundle qui en résulte est vraiment minime . C'est ce que j'appelle parfait.
Notre application est désormais entièrement intégrée à l'authentification Firebase dans les composants serveur et client. Nous sommes prêts à libérer tout le potentiel de Next.js !
Le code source de l'application se trouve dans next-firebase-auth-edge/examples/next-typescript-minimal
Dans ce guide, nous avons intégré la nouvelle application Next.js avec l'authentification Firebase.
Bien qu'assez détaillé, l'article a omis certaines parties importantes du flux d'authentification, telles que le formulaire de réinitialisation du mot de passe ou les méthodes de connexion autres que l'e-mail et le mot de passe.
Si vous êtes intéressé par la bibliothèque, vous pouvez prévisualiser la page de démonstration complète de next-firebase-auth-edge starter .
Il comprend l'intégration de Firestore , les actions du serveur , la prise en charge d'App-Check et plus encore.
La bibliothèque fournit une page de documentation dédiée avec des tonnes d'exemples
Si vous avez aimé l'article, j'apprécierais de mettre en vedette le référentiel next-firebase-auth-edge . Acclamations! 🎉
Ce guide bonus vous apprendra comment déployer votre application Next.js sur Vercel
Pour pouvoir déployer sur Vercel, vous devrez créer un référentiel pour votre nouvelle application.
Rendez-vous sur https://github.com/ et créez un nouveau référentiel.
create-next-app
a déjà lancé un dépôt git local pour nous, il vous suffit donc de suivre le dossier racine de votre projet et d'exécuter :
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
Allez sur https://vercel.com/ et connectez-vous avec votre compte Github
Une fois connecté, accédez à la page Présentation de Vercel et cliquez sur Ajouter un nouveau > Projet.
Cliquez sur Importer à côté du référentiel Github que nous venons de créer. Ne déployez pas encore.
Avant de déployer, nous devons fournir la configuration du projet. Ajoutons quelques variables d'environnement :
N'oubliez pas de définir USE_SECURE_COOKIES
sur true
, car Vercel utilise HTTPS par défaut
Maintenant, nous sommes prêts à cliquer sur Déployer
Attendez une minute ou deux et vous devriez pouvoir accéder à votre application avec une URL similaire à celle-ci : https://next-typescript-minimal-xi.vercel.app/
Fait. Je parie que vous ne vous attendiez pas à ce que ce soit aussi simple.
Si vous avez aimé le guide, j'apprécierais de mettre en vedette le référentiel next-firebase-auth-edge .
Vous pouvez également me faire part de vos retours dans les commentaires. Acclamations! 🎉