paint-brush
Utilisation de l'authentification Firebase avec les dernières fonctionnalités Next.jspar@awinogrodzki
3,584 lectures
3,584 lectures

Utilisation de l'authentification Firebase avec les dernières fonctionnalités Next.js

par Amadeusz Winogrodzki32m2024/04/04
Read on Terminal Reader

Trop long; Pour lire

Un guide complet, étape par étape, sur l'intégration de l'authentification Firebase avec Next.js à l'aide de la bibliothèque « next-firebase-auth-edge » de taille zéro. Il comprend des étapes pour les fonctionnalités d'enregistrement, de connexion et de déconnexion des utilisateurs, ainsi qu'une logique de redirection pour une expérience utilisateur transparente. Dans ce guide, vous allez apprendre à intégrer l'authentification Firebase aux dernières fonctionnalités de Next.js, telles que le routeur d'application, le middleware et les composants de serveur. Il se termine par des instructions sur le déploiement de l'application sur Vercel, présentant la facilité d'utilisation de la bibliothèque et sa conception évolutive pour les développeurs cherchant à améliorer leurs applications Next.js avec l'authentification Firebase.
featured image - Utilisation de l'authentification Firebase avec les dernières fonctionnalités Next.js
Amadeusz Winogrodzki HackerNoon profile picture
0-item
1-item

Introduction à 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.


Comment ça a commencé

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.


Comment ça va

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.



Mon autre moi



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.


Configuration de Firebase

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


Activation de la méthode de connexion par e-mail/mot de passe


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.


Récupérer la clé API Web


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.


Récupérer l'identifiant de l'expéditeur


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

Créer l'application Next.js à partir de zéro

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.

Configuration de l'application Next.js avec CLI

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 :


Page d'accueil de Next.js

Préparation des variables d'environnement

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

Intégration avec l'authentification Firebase

Installation de 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 };


Ajout d'un middleware

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 .


Création d'une page d'accueil sécurisée

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 !


Installer 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.

Création d'une page d'inscription

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 :


Page d'inscription


Création d'une page de connexion

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


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


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 :

  • Lorsqu'un utilisateur authentifié tente d'accéder aux pages /register et /login , nous devons le rediriger vers /
  • Lorsqu'un utilisateur non authentifié tente d'accéder à la page / , 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


Épilogue

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! 🎉



Bonus – Déploiement de l'application sur Vercel

Ce guide bonus vous apprendra comment déployer votre application Next.js sur Vercel

Création du dépôt git

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


Ajout d'un nouveau projet Vercel

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 :

Déploiement sur Vercel


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! 🎉