next-firebase-auth-edge
Probablemente encontró este artículo mientras buscaba formas de agregar Firebase Authentication a su aplicación Next.js nueva o existente. Su objetivo es tomar una decisión inteligente, imparcial y orientada al futuro que maximice las posibilidades de que su aplicación tenga éxito. Como creador de next-firebase-auth-edge, debo admitir que brindar una opinión completamente imparcial no es mi fuerte, pero al menos intentaré justificar el enfoque que adopté al diseñar la biblioteca. Con suerte, al final de esta guía, el enfoque le resultará sencillo y viable a largo plazo.
Te ahorraré largas presentaciones. Permítanme decirles que la idea de la biblioteca se inspiró en una situación posiblemente similar a la suya. Fue el momento en que Next.js lanzó una versión canaria de App Router . Estaba trabajando en una aplicación que dependía en gran medida de reescrituras y redirecciones internas. Para eso, utilizamos la aplicación Next.js de renderizado del servidor express
Node.js personalizado.
Estábamos muy entusiasmados con App Router y Server Components , pero sabíamos que no sería compatible con nuestro servidor personalizado. El middleware parecía una característica poderosa que podíamos aprovechar para eliminar la necesidad de un servidor Express personalizado, optando en su lugar por confiar únicamente en las características integradas de Next.js para redirigir y reescribir a los usuarios a diferentes páginas de forma dinámica.
Esa vez estábamos usando next-firebase-auth . Nos gustó mucho la biblioteca, pero extendió nuestra lógica de autenticación a través de archivos next.config.js
, pages/_app.tsx
, pages/api/login.ts
, pages/api/logout.ts
, que iban a considerarse heredados. pronto. Además, la biblioteca no era compatible con el middleware, lo que nos impedía reescribir las URL o redirigir a los usuarios según su contexto.
Entonces, comencé mi búsqueda, pero para mi sorpresa, no encontré ninguna biblioteca que admitiera la autenticación Firebase dentro del middleware. – ¿Por qué podría ser eso? ¡Es imposible! Como ingeniero de software con más de 11 años de experiencia comercial en Node.js y React, me estaba preparando para abordar este enigma.
Entonces comencé. Y la respuesta se hizo obvia. El middleware se ejecuta dentro de Edge Runtime . No hay ninguna biblioteca de base de fuego compatible con las API de Web Crypto disponibles dentro de Edge Runtime . Estaba condenado . Me sentí impotente. ¿Es esta la primera vez que tendré que esperar para poder jugar con las nuevas y sofisticadas API? - No. Una olla vigilada nunca hierve. Rápidamente dejé de sollozar y comencé a realizar ingeniería inversa en next-firebase-auth , firebase-admin y varias otras bibliotecas de autenticación JWT, adaptándolas a Edge Runtime. Aproveché la oportunidad para abordar todos los problemas que había encontrado con las bibliotecas de autenticación anteriores, con el objetivo de crear la biblioteca de autenticación más ligera, más fácil de configurar y orientada al futuro.
Aproximadamente dos semanas después, nació la versión 0.0.1
de next-firebase-auth-edge . Fue una prueba de concepto sólida, pero no querrás usar la versión 0.0.1
. Confía en mí.
Casi dos años después , estoy encantado de anunciar que después de 372 confirmaciones , 110 problemas resueltos y una gran cantidad de comentarios invaluables de increíbles desarrolladores de todo el mundo, la biblioteca ha llegado a una etapa en la que mi otro yo me muestra su aprobación.
En esta guía, usaré la versión 1.4.1 de next-firebase-auth-edge para crear una aplicación Next.js autenticada desde cero. Revisaremos cada paso en detalle, comenzando con la creación de un nuevo proyecto Firebase y la aplicación Next.js, seguido de la integración con las bibliotecas next-firebase-auth-edge
y firebase/auth
. Al final de este tutorial, implementaremos la aplicación en Vercel para confirmar que todo funciona tanto localmente como en un entorno listo para producción.
Esta parte supone que aún no has configurado la autenticación de Firebase. Si no, no dudes en pasar a la siguiente parte.
Vayamos a Firebase Console y creemos un proyecto.
Una vez creado el proyecto, habilitemos la autenticación de Firebase. Abra la consola y vaya a Compilación > Autenticación > Método de inicio de sesión y habilite el método de correo electrónico y contraseña . Ese es el método que vamos a admitir en nuestra aplicación.
Después de habilitar su primer método de inicio de sesión, la autenticación de Firebase debería estar habilitada para su proyecto y podrá recuperar su clave API web en la configuración del proyecto.
Copie la clave API y manténgala segura. Ahora, abramos la siguiente pestaña: Mensajería en la nube y anotemos el ID del remitente . Lo necesitaremos más tarde.
Por último, pero no menos importante, debemos generar las credenciales de la cuenta de servicio. Esto permitirá que su aplicación obtenga acceso completo a sus servicios de Firebase. Vaya a Configuración del proyecto > Cuentas de servicio y haga clic en Generar nueva clave privada . Esto descargará un archivo .json
con las credenciales de la cuenta de servicio. Guarde este archivo en una ubicación conocida.
¡Eso es todo! Estamos listos para integrar la aplicación Next.js con Firebase Authentication
Esta guía asume que tiene Node.js y npm instalados. Los comandos utilizados en este tutorial se verificaron con la última versión de LTS Node.js v20 . Puede verificar la versión del nodo ejecutando node -v
en la terminal. También puedes utilizar herramientas como NVM para cambiar rápidamente entre versiones de Node.js.
Abra su terminal favorita, navegue hasta su carpeta de proyectos y ejecute
npx create-next-app@latest
Para hacerlo simple, usemos la configuración predeterminada. Esto significa que usaremos TypeScript
y 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
Naveguemos hasta el directorio raíz del proyecto y asegurémonos de que todas las dependencias estén instaladas.
cd my-app npm install
Para confirmar que todo funciona como se esperaba, iniciemos el servidor de desarrollo Next.js con el comando npm run dev
. Cuando abres http://localhost:3000 , deberías ver la página de bienvenida de Next.js, similar a esta:
Antes de comenzar a integrarnos con Firebase, necesitamos una forma segura de almacenar y leer nuestra configuración de Firebase. Afortunadamente, Next.js viene con soporte dotenv incorporado.
Abra su editor de código favorito y navegue hasta la carpeta del proyecto.
Creemos el archivo .env.local
en el directorio raíz del proyecto y llenémoslo con las siguientes variables de entorno:
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=...
Tenga en cuenta que las variables con el prefijo NEXT_PUBLIC_
estarán disponibles en el paquete del lado del cliente. Los necesitaremos para configurar el SDK del cliente de autenticación de Firebase
NEXT_PUBLIC_FIREBASE_PROJECT_ID
, FIREBASE_ADMIN_CLIENT_EMAIL
y FIREBASE_ADMIN_PRIVATE_KEY
se pueden recuperar desde el archivo .json
descargado después de generar las credenciales de la cuenta de servicio
AUTH_COOKIE_NAME
será el nombre de la cookie utilizada para almacenar las credenciales del usuario.
AUTH_COOKIE_SIGNATURE_KEY_CURRENT
y AUTH_COOKIE_SIGNATURE_KEY_PREVIOUS
son secretos con los que firmaremos las credenciales.
NEXT_PUBLIC_FIREBASE_API_KEY
es la clave API web recuperada de la página general de Configuración del proyecto
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN
es su ID de proyecto .firebaseapp.com
NEXT_PUBLIC_FIREBASE_DATABASE_URL
es el ID de su proyecto .firebaseio.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID
se puede obtener en la página Configuración del proyecto > Mensajería en la nube
USE_SECURE_COOKIES
no se usará para el desarrollo local, pero será útil cuando implementemos nuestra aplicación en Vercel.
next-firebase-auth-edge
y configuración inicial Agregue la biblioteca a las dependencias del proyecto ejecutando npm install next-firebase-auth-edge@^1.4.1
Creemos el archivo config.ts
para encapsular la configuración de nuestro proyecto. No es obligatorio, pero hará que los ejemplos de código sean más legibles.
No pierda demasiado tiempo reflexionando sobre esos valores. Los explicaremos con más detalle a medida que avancemos.
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 };
Cree el archivo middleware.ts
en la raíz del proyecto y pegue lo siguiente
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", ], };
Lo creas o no, acabamos de integrar el servidor de nuestra aplicación con Firebase Authentication. Antes de usarlo, expliquemos un poco la configuración:
loginPath
le indicará a authMiddleware que exponga el punto final GET /api/login
. Cuando se llama a este punto final con el encabezado Authorization: Bearer ${idToken}
*, responde con el encabezado Set-Cookie
solo HTTP(S) que contiene tokens personalizados y de actualización firmados.
* idToken
se recupera con la función getIdToken
disponible en Firebase Client SDK . Más sobre esto más adelante.
De manera similar, logoutPath
indica al middleware que exponga GET /api/logout
, pero no requiere ningún encabezado adicional. Cuando se llama, elimina las cookies de autenticación del navegador.
apiKey
es la clave API web. El middleware lo utiliza para actualizar el token personalizado y restablecer las cookies de autenticación una vez que caducan las credenciales.
cookieName
es el nombre de la cookie establecida y eliminada por los puntos finales /api/login
y /api/logout
cookieSignatureKeys
es una lista de claves secretas con las que se firman las credenciales del usuario. Las credenciales siempre se firmarán con la primera clave de la lista, por lo que deberá proporcionar al menos un valor. Puede proporcionar varias claves para realizar una rotación de claves
cookieSerializeOptions
son opciones que se pasan a la cookie al generar el encabezado Set-Cookie
. Ver cookie README para más información
serviceAccount
autoriza a la biblioteca a utilizar sus servicios de Firebase.
El comparador indica al servidor Next.js que ejecute Middleware contra /api/login
, /api/logout
/
cualquier otra ruta que no sea un archivo o una llamada a la API.
export const config = { matcher: [ "/", "/((?!_next|api|.*\\.).*)", "/api/login", "/api/logout", ], };
Quizás se pregunte por qué no habilitamos el middleware para todas las llamadas /api/*
. Podríamos, pero es una buena práctica manejar llamadas no autenticadas dentro del propio controlador de ruta API. Esto está un poco fuera del alcance de este tutorial, pero si estás interesado, ¡házmelo saber y prepararé algunos ejemplos!
Como puedes ver, la configuración es mínima y con un propósito claramente definido. Ahora, comencemos a llamar a nuestros puntos finales /api/login
y /api/logout
.
Para simplificar al máximo las cosas, eliminemos la página de inicio predeterminada de Next.js y reemplácela con contenido personalizado.
Abra ./app/page.tsx
y pegue esto:
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> ); }
Analicemos esto poco a poco.
La función getTokens
está diseñada para validar y extraer las credenciales de usuario de las cookies.
const tokens = await getTokens(cookies(), { apiKey: clientConfig.apiKey, cookieName: serverConfig.cookieName, cookieSignatureKeys: serverConfig.cookieSignatureKeys, serviceAccount: serverConfig.serviceAccount, });
Se resuelve con null
, si el usuario no está autenticado o con un objeto que contiene dos propiedades:
token
, que es string
idToken que puede utilizar para autorizar solicitudes de API a servicios backend externos. Esto está un poco fuera de alcance, pero vale la pena mencionar que la biblioteca permite la arquitectura de servicios distribuidos. El token
es compatible y está listo para usar con todas las bibliotecas oficiales de Firebase en todas las plataformas.
decodedToken
como sugiere el nombre, es una versión decodificada del token
, que contiene toda la información necesaria para identificar al usuario, incluida la dirección de correo electrónico, la imagen de perfil y los reclamos personalizados , lo que nos permite además restringir el acceso según roles y permisos.
Después de obtener tokens
, utilizamos la función notFound de next/navigation
para asegurarnos de que solo los usuarios autenticados puedan acceder a la página.
if (!tokens) { notFound(); }
Finalmente, presentamos contenido de usuario básico y personalizado.
<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>
Ejecutémoslo.
En caso de que haya cerrado su servidor de desarrollo, simplemente ejecute npm run dev
.
Cuando intenta acceder a http://localhost:3000/ , debería ver 404: No se pudo encontrar esta página.
¡Éxito! ¡Hemos mantenido nuestros secretos a salvo de miradas indiscretas!
firebase
e inicialización del SDK del cliente de Firebase Ejecute npm install firebase
en el directorio raíz del proyecto
Después de instalar el SDK del cliente, cree el archivo firebase.ts
en el directorio raíz del proyecto y pegue lo siguiente
import { initializeApp } from 'firebase/app'; import { clientConfig } from './config'; export const app = initializeApp(clientConfig);
Esto inicializará el SDK de Firebase Client y expondrá el objeto de la aplicación para los componentes del cliente.
¿Cuál es el punto de tener una página de inicio súper segura si nadie puede verla? Creemos una página de registro simple para permitir que las personas accedan a nuestra aplicación.
Creemos una página nueva y elegante en ./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> ); }
Lo sé. Es mucho texto, pero tengan paciencia.
Empezamos con "use client";
para indicar que la página de registro utilizará API del lado del cliente
const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmation, setConfirmation] = useState(""); const [error, setError] = useState("");
Luego, definimos algunas variables y configuradores para mantener nuestro estado de formulario.
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); } }
Aquí, definimos nuestra lógica de envío de formularios. Primero, validamos si password
y confirmation
son iguales, de lo contrario actualizamos el estado de error. Si los valores son válidos, creamos una cuenta de usuario con createUserWithEmailAndPassword
desde firebase/auth
. Si este paso falla (por ejemplo, se toma el correo electrónico), informamos al usuario actualizando el error.
Si todo va bien, redirigimos al usuario a la página /login
. Probablemente estés confundido en este momento y tienes razón en estarlo. /login
aún no existe. Sólo nos estamos preparando para lo que será el próximo.
Cuando visita http://localhost:3000/register , la página debería verse más o menos así:
Ahora que los usuarios pueden registrarse, que acrediten su identidad.
Cree una página de inicio de sesión en ./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> ); }
Como puede ver, es bastante similar a la página de registro. Centrémonos en la parte 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); } }
Es donde ocurre toda la magia . Usamos signInEmailAndPassword
de firebase/auth
para recuperar idToken
del usuario.
Luego, llamamos al punto final /api/login
expuesto por el middleware. Este punto final actualiza las cookies de nuestro navegador con las credenciales del usuario.
Finalmente, redirigimos al usuario a la página de inicio llamando router.push("/");
La página de inicio de sesión debería verse aproximadamente como esta
¡Vamos a probarlo!
Vaya a http://localhost:3000/register , ingrese una dirección de correo electrónico y una contraseña aleatorias para crear una cuenta. Utilice esas credenciales en la página http://localhost:3000/login . Después de hacer clic en Entrar , debería ser redirigido a una página de inicio súper segura.
¡Finalmente podemos ver nuestra propia página de inicio personal y ultra segura ! ¡Pero espera! ¿Cómo salimos?
Necesitamos agregar un botón de cierre de sesión para no excluirnos del mundo para siempre (o 12 días).
Antes de comenzar, necesitamos crear un componente de cliente que pueda cerrar sesión utilizando Firebase Client SDK.
Creemos un nuevo archivo en ./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> ); }
Como habrás notado, esta es una versión ligeramente modificada de nuestro ./app/page.tsx
. Tuvimos que crear un componente de cliente separado, porque getTokens
solo funciona dentro de los componentes del servidor y los controladores de ruta API , mientras que signOut
y useRouter
deben ejecutarse en el contexto del cliente. Un poco complicado, lo sé, pero en realidad es bastante poderoso. Te lo explicaré más tarde.
Centrémonos en el proceso de cierre de sesión.
const router = useRouter(); async function handleLogout() { await signOut(getAuth(app)); await fetch("/api/logout"); router.push("/login"); }
Primero, cerramos sesión en Firebase Client SDK. Luego, llamamos al punto final /api/logout
expuesto por el middleware. Terminamos redirigiendo al usuario a la página /login
.
Actualicemos la página de inicio de nuestro servidor. Vaya a ./app/page.tsx
y pegue lo siguiente
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} />; }
Ahora, nuestro componente del servidor Home
es responsable únicamente de obtener tokens de usuario y pasarlos al componente del cliente HomePage
. En realidad, este es un patrón bastante común y útil.
Probemos esto:
¡Voilá! Ahora podemos iniciar sesión y cerrar sesión en la aplicación a nuestra voluntad. ¡Eso es perfecto!
¿O es eso?
Cuando un usuario no autenticado intenta ingresar a la página de inicio abriendo http://localhost:3000/, mostramos 404: No se pudo encontrar esta página.
Además, los usuarios autenticados aún pueden acceder a las páginas http://localhost:3000/register y http://localhost:3000/login sin tener que cerrar sesión.
Podemos hacerlo mejor.
Parece que necesitamos agregar alguna lógica de redireccionamiento. Definamos algunas reglas:
/register
y /login
, debemos redirigirlo a /
/
, debemos redirigirlo a /login
El middleware es una de las mejores formas de manejar redirecciones en aplicaciones Next.js. Afortunadamente, authMiddleware
admite varias opciones y funciones auxiliares para manejar una amplia gama de escenarios de redireccionamiento.
Abramos el archivo middleware.ts
y peguemos esta versión actualizada.
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", ], };
Eso debería ser todo. Hemos implementado todas las reglas de redireccionamiento. Analicemos esto.
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 } }); },
Se llama a handleValidToken
cuando se adjuntan credenciales de usuario válidas a la solicitud, es decir. El usuario está autenticado. Se llama con el objeto tokens
como primer argumento y los encabezados de solicitud modificados como segundo argumento. Debería resolverse con NextResponse
.
redirectToHome
de next-firebase-auth-edge
es una función auxiliar que devuelve un objeto que se puede simplificar a NextResponse.redirect(new URL(“/“))
Al marcar PUBLIC_PATHS.includes(request.nextUrl.pathname)
, validamos si el usuario autenticado intenta acceder a la página /login
o /register
, y redirigimos a inicio si ese es el caso.
handleInvalidToken: async (reason) => { console.info('Missing or malformed credentials', {reason}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); },
Se llama handleInvalidToken
cuando sucede algo esperado . Uno de estos eventos esperados es que el usuario vea su aplicación por primera vez, desde otro dispositivo o después de que las credenciales hayan caducado.
Habiendo sabido que se llama a handleInvalidToken
para usuarios no autenticados, podemos proceder con la segunda regla: cuando un usuario no autenticado intenta acceder a /
página, debemos redirigirlo a /login
Como no hay ninguna otra condición que cumplir, simplemente devolvemos el resultado de redirectToLogin
que se puede simplificar a NextResponse.redirect(new URL(“/login”))
. También garantiza que el usuario no caiga en un bucle de redireccionamiento.
Por último,
handleError: async (error) => { console.error('Unhandled authentication error', {error}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); }
A diferencia de handleInvalidToken
, se llama handleError
cuando sucede algo inesperado* y posiblemente deba investigarse. Puedes encontrar una lista de posibles errores con su descripción en la documentación.
En caso de error, registramos este hecho y redirigimos al usuario de forma segura a la página de inicio de sesión.
* Se puede llamar handleError
con el error INVALID_ARGUMENT
después de actualizar las claves públicas de Google.
Esta es una forma de rotación de claves y se espera . Consulte este problema de Github para obtener más información.
Eso es todo. Finalmente.
Cerremos sesión en nuestra aplicación web y abramos http://localhost:3000/ . Deberíamos ser redirigidos a la página /login
.
Iniciemos sesión nuevamente e intentemos ingresar http://localhost:3000/login . Deberíamos ser redirigidos a /
página.
No solo brindamos una experiencia de usuario perfecta. next-firebase-auth-edge
es una biblioteca de tamaño de paquete cero que funciona solo en el servidor de la aplicación y no introduce código adicional del lado del cliente. El paquete resultante es realmente mínimo . Eso es lo que yo llamo perfecto.
Nuestra aplicación ahora está completamente integrada con Firebase Authentication tanto en los componentes del Servidor como del Cliente. ¡Estamos listos para liberar todo el potencial de Next.js!
El código fuente de la aplicación se puede encontrar en next-firebase-auth-edge/examples/next-typescript-minimal
En esta guía, analizamos la integración de la nueva aplicación Next.js con Firebase Authentication.
Aunque es bastante extenso, el artículo omitió algunas partes importantes del flujo de autenticación, como el formulario de restablecimiento de contraseña o métodos de inicio de sesión distintos del correo electrónico y la contraseña.
Si está interesado en la biblioteca, puede obtener una vista previa de la página de demostración inicial completa de next-firebase-auth-edge .
Cuenta con integración de Firestore , acciones del servidor , compatibilidad con App-Check y más
La biblioteca proporciona una página de documentación dedicada con toneladas de ejemplos.
Si le gustó el artículo, le agradecería destacar el repositorio next-firebase-auth-edge . ¡Salud! 🎉
Esta guía adicional le enseñará cómo implementar su aplicación Next.js en Vercel.
Para poder implementar en Vercel, deberá crear un repositorio para su nueva aplicación.
Dirígete a https://github.com/ y crea un nuevo repositorio.
create-next-app
ya inició un repositorio git local para nosotros, por lo que solo necesita ir a la carpeta raíz de su proyecto y ejecutar:
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
Vaya a https://vercel.com/ e inicie sesión con su cuenta de Github
Después de haber iniciado sesión, vaya a la página Descripción general de Vercel y haga clic en Agregar nuevo > Proyecto.
Haga clic en Importar junto al repositorio de Github que acabamos de crear. No lo implementes todavía.
Antes de implementar, debemos proporcionar la configuración del proyecto. Agreguemos algunas variables de entorno:
Recuerde configurar USE_SECURE_COOKIES
en true
, ya que Vercel usa HTTPS de forma predeterminada
Ahora estamos listos para hacer clic en Implementar.
Espere uno o dos minutos y debería poder acceder a su aplicación con una URL similar a esta: https://next-typescript-minimal-xi.vercel.app/
Hecho. Apuesto a que no esperabas que fuera tan fácil.
Si le gustó la guía, le agradecería destacar el repositorio next-firebase-auth-edge .
También puedes dejarme saber tus comentarios en los comentarios. ¡Salud! 🎉