paint-brush
Usando o Firebase Authentication com os recursos Next.js mais recentespor@awinogrodzki
3,762 leituras
3,762 leituras

Usando o Firebase Authentication com os recursos Next.js mais recentes

por Amadeusz Winogrodzki32m2024/04/04
Read on Terminal Reader

Muito longo; Para ler

Um guia passo a passo abrangente sobre a integração do Firebase Authentication com Next.js usando a biblioteca `next-firebase-auth-edge` de tamanho zero. Inclui etapas para funcionalidades de registro, login e logout do usuário, juntamente com lógica de redirecionamento para uma experiência de usuário perfeita. Neste guia, você aprenderá como integrar o Firebase Authentication com os recursos mais recentes do Next.js, como App Router, Middleware e Server Components. Ele conclui com instruções sobre como implantar o aplicativo no Vercel, mostrando a facilidade de uso da biblioteca e o design preparado para o futuro para desenvolvedores que buscam aprimorar seus aplicativos Next.js com Firebase Authentication.
featured image - Usando o Firebase Authentication com os recursos Next.js mais recentes
Amadeusz Winogrodzki HackerNoon profile picture
0-item
1-item

Introdução ao next-firebase-auth-edge

Você provavelmente encontrou este artigo enquanto procurava maneiras de adicionar o Firebase Authentication ao seu aplicativo Next.js existente ou novo. Seu objetivo é tomar uma decisão inteligente, imparcial e voltada para o futuro que maximizará as chances de sucesso do seu aplicativo. Como criador do next-firebase-auth-edge, devo admitir que fornecer uma opinião totalmente imparcial não é meu forte, mas pelo menos tentarei justificar a abordagem que adotei ao projetar a biblioteca. Esperançosamente, ao final deste guia, você poderá achar a abordagem simples e viável a longo prazo.


Como isso começou

Vou poupar-lhe longas apresentações. Deixe-me apenas dizer que a ideia da biblioteca foi inspirada em uma situação possivelmente semelhante à sua. Foi a época em que Next.js lançou uma versão canário do App Router . Eu estava trabalhando em um aplicativo que dependia muito de reescritas e redirecionamentos internos. Para isso, estávamos usando o aplicativo Next.js de renderização de servidor express Node.js personalizado.


Ficamos muito entusiasmados com o App Router e o Server Components , mas cientes de que não seria compatível com nosso servidor personalizado. O middleware parecia um recurso poderoso que poderíamos aproveitar para eliminar a necessidade de um servidor Express personalizado, optando por confiar apenas nos recursos integrados do Next.js para redirecionar e reescrever usuários para páginas diferentes dinamicamente.


Naquela época, estávamos usando next-firebase-auth . Gostamos muito da biblioteca, mas ela espalhou nossa lógica de autenticação por meio de arquivos next.config.js , pages/_app.tsx , pages/api/login.ts , pages/api/logout.ts , que seriam considerados legados em breve. Além disso, a biblioteca não era compatível com o middleware, impedindo-nos de reescrever URLs ou redirecionar usuários com base em seu contexto.


Então, comecei minha pesquisa, mas para minha surpresa, não encontrei nenhuma biblioteca que suportasse o Firebase Authentication no middleware. – Por que isso poderia ser? É impossível! Como engenheiro de software com mais de 11 anos de experiência comercial em Node.js e React, eu estava me preparando para enfrentar esse enigma.


Então, eu comecei. E a resposta tornou-se óbvia. O middleware está sendo executado dentro do Edge Runtime . Não há biblioteca Firebase compatível com APIs Web Crypto disponíveis no Edge Runtime . Eu estava condenado . Eu me senti impotente. É a primeira vez que terei que esperar para começar a brincar com as novas e sofisticadas APIs? - Não. Uma panela vigiada nunca ferve. Rapidamente parei de chorar e comecei a fazer engenharia reversa next-firebase-auth , firebase-admin e várias outras bibliotecas de autenticação JWT, adaptando-as ao Edge Runtime. Aproveitei a oportunidade para resolver todos os problemas que encontrei com bibliotecas de autenticação anteriores, com o objetivo de criar a biblioteca de autenticação mais leve, mais fácil de configurar e voltada para o futuro.


Cerca de duas semanas depois, nasceu a versão 0.0.1 do next-firebase-auth-edge . Foi uma prova de conceito sólida, mas você não gostaria de usar a versão 0.0.1 . Confie em mim.


Como está indo

Quase dois anos depois , estou emocionado em anunciar que, após 372 commits , 110 problemas resolvidos e um monte de feedback inestimável de desenvolvedores incríveis em todo o mundo, a biblioteca atingiu um estágio em que meus outros eus me aprovam.



Meu outro eu



Neste guia, usarei a versão 1.4.1 do next-firebase-auth-edge para criar um aplicativo Next.js autenticado do zero. Passaremos por cada etapa detalhadamente, começando com a criação de um novo projeto Firebase e aplicativo Next.js, seguido pela integração com as bibliotecas next-firebase-auth-edge e firebase/auth . Ao final deste tutorial, iremos implantar o aplicativo no Vercel para confirmar se tudo está funcionando tanto localmente quanto em ambiente pronto para produção.


Configurando o Firebase

Esta parte pressupõe que você ainda não configurou o Firebase Authentication. Sinta-se à vontade para pular para a próxima parte, caso contrário.


Vamos para o Firebase Console e criar um projeto


Após a criação do projeto, vamos ativar o Firebase Authentication. Abra o console e siga para Construir > Autenticação > Método de login e habilite o método de e-mail e senha . Esse é o método que vamos apoiar em nosso aplicativo


Ativando o método de login por e-mail/senha


Depois de ativar seu primeiro método de login, o Firebase Authentication deverá ser ativado para seu projeto e você poderá recuperar sua chave de API da Web nas configurações do projeto


Recuperar chave de API da Web


Copie a chave API e mantenha-a segura. Agora, vamos abrir a próxima guia – Cloud Messaging e anotar o Sender ID . Vamos precisar disso mais tarde.


Recuperar ID do remetente


Por último, mas não menos importante, precisamos gerar credenciais de conta de serviço. Isso permitirá que seu aplicativo obtenha acesso total aos serviços do Firebase. Vá para Configurações do projeto > Contas de serviço e clique em Gerar nova chave privada . Isso fará o download de um arquivo .json com credenciais da conta de serviço. Salve este arquivo em um local conhecido.



É isso! Estamos prontos para integrar o aplicativo Next.js ao Firebase Authentication

Criando aplicativo Next.js do zero

Este guia pressupõe que você tenha o Node.js e o npm instalados. Os comandos usados neste tutorial foram verificados em relação ao LTS Node.js v20 mais recente. Você pode verificar a versão do nó executando node -v no terminal. Você também pode usar ferramentas como NVM para alternar rapidamente entre as versões do Node.js.

Configurando o aplicativo Next.js com CLI

Abra seu terminal favorito, navegue até a pasta de projetos e execute

 npx create-next-app@latest


Para simplificar, vamos usar a configuração padrão. Isso significa que usaremos TypeScript e 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


Vamos navegar até o diretório raiz do projeto e verificar se todas as dependências estão instaladas

 cd my-app npm install


Para confirmar que tudo funciona conforme o esperado, vamos iniciar o servidor de desenvolvimento Next.js com o comando npm run dev . Ao abrir http://localhost:3000 , você deverá ver a página de boas-vindas do Next.js, semelhante a esta:


Página de boas-vindas do Next.js

Preparando variáveis de ambiente

Antes de começarmos a integração com o Firebase, precisamos de uma maneira segura de armazenar e ler nossa configuração do Firebase. Felizmente, Next.js vem com suporte integrado a dotenv .


Abra seu editor de código favorito e navegue até a pasta do projeto


Vamos criar o arquivo .env.local no diretório raiz do projeto e preenchê-lo com as seguintes variáveis de ambiente:


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


Observe que as variáveis prefixadas com NEXT_PUBLIC_ estarão disponíveis no pacote do lado do cliente. Precisaremos deles para configurar o Firebase Auth Client SDK


NEXT_PUBLIC_FIREBASE_PROJECT_ID , FIREBASE_ADMIN_CLIENT_EMAIL e FIREBASE_ADMIN_PRIVATE_KEY podem ser recuperados do arquivo .json baixado após gerar credenciais de conta de serviço


AUTH_COOKIE_NAME será o nome do cookie usado para armazenar credenciais do usuário

AUTH_COOKIE_SIGNATURE_KEY_CURRENT e AUTH_COOKIE_SIGNATURE_KEY_PREVIOUS são segredos com os quais assinaremos as credenciais


NEXT_PUBLIC_FIREBASE_API_KEY é a chave da API da Web recuperada da página geral de configurações do projeto

NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN é o ID do seu projeto .firebaseapp.com

NEXT_PUBLIC_FIREBASE_DATABASE_URL é o ID do seu projeto .firebaseio.com

NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID pode ser obtido em Configurações do projeto > página Mensagens na nuvem


USE_SECURE_COOKIES não será usado para desenvolvimento local, mas será útil quando implantarmos nosso aplicativo no Vercel

Integração com autenticação do Firebase

Instalando next-firebase-auth-edge e configuração inicial

Adicione a biblioteca às dependências do projeto executando npm install next-firebase-auth-edge@^1.4.1


Vamos criar o arquivo config.ts para encapsular a configuração do nosso projeto. Não é obrigatório, mas tornará os exemplos de código mais legíveis.

Não perca muito tempo ponderando sobre esses valores. Iremos explicá-los com mais detalhes à medida que prosseguirmos.


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


Adicionando Middleware

Crie o arquivo middleware.ts na raiz do projeto e cole o seguinte

 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", ], };


Acredite ou não, acabamos de integrar o servidor do nosso aplicativo ao Firebase Authentication. Antes de realmente usá-lo, vamos explicar um pouco a configuração:


loginPath instruirá o authMiddleware a expor o endpoint GET /api/login . Quando este endpoint é chamado com o cabeçalho Authorization: Bearer ${idToken} *, ele responde com o cabeçalho HTTP(S)-Only Set-Cookie contendo tokens personalizados e de atualização assinados


* idToken é recuperado com a função getIdToken disponível no Firebase Client SDK . Mais sobre isso mais tarde.


Da mesma forma, logoutPath instrui o middleware a expor GET /api/logout , mas não requer nenhum cabeçalho adicional. Quando chamado, remove cookies de autenticação do navegador.


apiKey é a chave da API da Web. O middleware o utiliza para atualizar o token personalizado e redefinir os cookies de autenticação após as credenciais expirarem.


cookieName é o nome do cookie definido e removido pelos endpoints /api/login e /api/logout


cookieSignatureKeys uma lista de chaves secretas com as quais as credenciais do usuário são assinadas. As credenciais sempre serão assinadas com a primeira chave da lista, portanto você precisa fornecer pelo menos um valor. Você pode fornecer várias chaves para realizar uma rotação de chaves


cookieSerializeOptions são opções passadas para cookie ao gerar o cabeçalho Set-Cookie . Consulte o README do cookie para obter mais informações


serviceAccount autoriza a biblioteca a usar seus serviços do Firebase.


O matcher instrui o servidor Next.js a executar o Middleware em /api/login , /api/logout , / e qualquer outro caminho que não seja um arquivo ou chamada de API.

 export const config = { matcher: [ "/", "/((?!_next|api|.*\\.).*)", "/api/login", "/api/logout", ], };


Você deve estar se perguntando por que não habilitamos o middleware para todas as chamadas /api/* . Poderíamos, mas é uma boa prática lidar com chamadas não autenticadas dentro do próprio manipulador de rotas da API. Isso está um pouco fora do escopo deste tutorial, mas se você estiver interessado, me avise e prepararei alguns exemplos!



Como você pode ver, a configuração é mínima e com finalidade claramente definida. Agora, vamos começar a chamar nossos endpoints /api/login e /api/logout .


Criando uma página inicial segura

Para tornar as coisas o mais simples possível, vamos limpar a página inicial padrão do Next.js e substituí-la por algum conteúdo personalizado


Abra ./app/page.tsx e cole isto:

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


Vamos analisar isso pouco a pouco.


A função getTokens foi projetada para validar e extrair credenciais de usuário de cookies

 const tokens = await getTokens(cookies(), { apiKey: clientConfig.apiKey, cookieName: serverConfig.cookieName, cookieSignatureKeys: serverConfig.cookieSignatureKeys, serviceAccount: serverConfig.serviceAccount, });


Ele resolve com null , se o usuário não estiver autenticado ou com um objeto contendo duas propriedades:


token que é string idToken que você pode usar para autorizar solicitações de API para serviços de back-end externos. Isso está um pouco fora do escopo, mas vale a pena mencionar que a biblioteca permite arquitetura de serviços distribuídos. O token é compatível e pronto para uso com todas as bibliotecas oficiais do Firebase em todas as plataformas.


decodedToken como o nome sugere, é uma versão decodificada do token , que contém todas as informações necessárias para identificar o usuário, incluindo endereço de e-mail, foto do perfil e declarações personalizadas , o que nos permite restringir ainda mais o acesso com base em funções e permissões.


Depois de obter tokens , usamos a função notFound de next/navigation para garantir que a página esteja acessível apenas para usuários autenticados

 if (!tokens) { notFound(); }


Por fim, renderizamos algum conteúdo básico e personalizado do usuário

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


Vamos executá-lo.

Caso você tenha fechado seu servidor de desenvolvimento, basta executar npm run dev .


Ao tentar acessar http://localhost:3000/ , você deverá ver 404: Esta página não foi encontrada.


Sucesso! Mantivemos nossos segredos protegidos de olhares indiscretos!


Instalando firebase e inicializando o Firebase Client SDK

Execute npm install firebase no diretório raiz do projeto


Após a instalação do SDK do cliente, crie o arquivo firebase.ts no diretório raiz do projeto e cole o seguinte

 import { initializeApp } from 'firebase/app'; import { clientConfig } from './config'; export const app = initializeApp(clientConfig);


Isso inicializará o Firebase Client SDK e exporá o objeto do aplicativo para componentes do cliente

Criando uma página de registro

Qual é o sentido de ter uma página inicial supersegura se ninguém consegue visualizá-la? Vamos construir uma página de registro simples para permitir que as pessoas acessem nosso aplicativo.


Vamos criar uma página nova e sofisticada em ./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> ); }


Eu sei. É muito texto, mas tenha paciência comigo.


Começamos com "use client"; para indicar que a página de registro usará APIs do lado do cliente


 const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmation, setConfirmation] = useState(""); const [error, setError] = useState("");

Então, definimos algumas variáveis e setters para manter o estado do nosso formulário


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

Aqui, definimos nossa lógica de envio de formulário. Primeiro validamos se password e confirmation são iguais, caso contrário atualizamos o estado de erro. Se os valores forem válidos, criamos uma conta de usuário com createUserWithEmailAndPassword de firebase/auth . Se esta etapa falhar (por exemplo, o e-mail for recebido), informaremos o usuário atualizando o erro.


Se tudo correr bem, redirecionamos o usuário para a página /login . Você provavelmente está confuso agora e está certo em estar assim. A página /login ainda não existe. Estamos apenas nos preparando para o que virá a seguir.


Quando você visita http://localhost:3000/register , a página deve ficar mais ou menos assim:


Página de registro


Criando uma página de login

Agora que os usuários podem se cadastrar, deixe-os comprovar sua identidade


Crie uma página de login em ./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> ); }


Como você pode ver, é muito semelhante à página de registro. Vamos nos concentrar na 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); } }


É onde toda a magia acontece. Usamos signInEmailAndPassword do firebase/auth para recuperar idToken do usuário.


Em seguida, chamamos o endpoint /api/login exposto pelo middleware. Este endpoint atualiza os cookies do nosso navegador com credenciais do usuário.


Finalmente, redirecionamos o usuário para a página inicial chamando router.push("/");


A página de login deve ser mais ou menos assim



Vamos testar!


Vá para http://localhost:3000/register , digite algum endereço de e-mail aleatório e senha para criar uma conta. Use essas credenciais na página http://localhost:3000/login . Depois de clicar em Enter , você será redirecionado para a página inicial super segura


Página inicial super segura




Finalmente conseguimos ver nossa página inicial pessoal e ultra segura ! Mas espere! Como saímos?


Precisamos adicionar um botão de logout para não nos isolarmos do mundo para sempre (ou 12 dias).


Antes de começarmos, precisamos criar um componente cliente que será capaz de nos desconectar usando o Firebase Client SDK.


Vamos criar um novo arquivo em ./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 você deve ter notado, esta é uma versão ligeiramente modificada do nosso ./app/page.tsx . Tivemos que criar um componente cliente separado, porque getTokens funciona apenas dentro de componentes de servidor e manipuladores de rotas de API , enquanto signOut e useRouter precisam ser executados no contexto do cliente. Um pouco complicado, eu sei, mas na verdade é bastante poderoso. Eu vou explicar mais tarde.


Vamos nos concentrar no processo de logout

 const router = useRouter(); async function handleLogout() { await signOut(getAuth(app)); await fetch("/api/logout"); router.push("/login"); }


Primeiro, saímos do Firebase Client SDK. Em seguida, chamamos o endpoint /api/logout exposto pelo middleware. Terminamos redirecionando o usuário para a página /login .


Vamos atualizar a página inicial do nosso servidor. Vá para ./app/page.tsx e cole o seguinte

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


Agora, nosso componente de servidor Home é responsável apenas por buscar tokens de usuário e passá-los para o componente cliente HomePage . Na verdade, esse é um padrão bastante comum e útil.


Vamos testar isso:


Voilá! Agora podemos fazer login e logout do aplicativo conforme nossa vontade. Perfeito!

Ou é?


Quando um usuário não autenticado tenta entrar na página inicial abrindo http://localhost:3000/ mostramos 404: Esta página não foi encontrada.

Além disso, os usuários autenticados ainda podem acessar as páginas http://localhost:3000/register e http://localhost:3000/login sem precisar fazer logout.


Podemos fazer melhor.


Parece que precisamos adicionar alguma lógica de redirecionamento. Vamos definir algumas regras:

  • Quando um usuário autenticado tenta acessar as páginas /register e /login , devemos redirecioná-lo para /
  • Quando um usuário não autenticado tenta acessar / página, devemos redirecioná-lo para /login


Middleware é uma das melhores maneiras de lidar com redirecionamentos em aplicativos Next.js. Felizmente, authMiddleware oferece suporte a várias opções e funções auxiliares para lidar com uma ampla variedade de cenários de redirecionamento.


Vamos abrir o arquivo middleware.ts e colar esta versão atualizada

 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", ], };


Deveria ser isso. Implementamos todas as regras de redirecionamento. Vamos analisar isso.


 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 é chamado quando credenciais de usuário válidas são anexadas à solicitação, ou seja. usuário está autenticado. Ele é chamado com o objeto tokens como o primeiro e os cabeçalhos de solicitação modificados como o segundo argumento. Deve resolver com NextResponse .


redirectToHome de next-firebase-auth-edge é uma função auxiliar que retorna um objeto que pode ser simplificado para NextResponse.redirect(new URL(“/“))


Ao verificar PUBLIC_PATHS.includes(request.nextUrl.pathname) , validamos se o usuário autenticado tenta acessar a página /login ou /register e redirecionamos para home se for o caso.



 handleInvalidToken: async (reason) => { console.info('Missing or malformed credentials', {reason}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); },


handleInvalidToken é chamado quando algo esperado acontece. Um desses eventos esperados é o usuário ver seu aplicativo pela primeira vez, em outro dispositivo ou após as credenciais expirarem.


Sabendo que handleInvalidToken é chamado para usuário não autenticado, podemos prosseguir com a segunda regra: Quando um usuário não autenticado tenta acessar / página, devemos redirecioná-lo para /login


Como não há outra condição a atender, apenas retornamos o resultado de redirectToLogin que pode ser simplificado para NextResponse.redirect(new URL(“/login”)) . Também garante que o usuário não caia no loop de redirecionamento.


Por último,

 handleError: async (error) => { console.error('Unhandled authentication error', {error}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); }


Ao contrário de handleInvalidToken , handleError é chamado quando algo inesperado* acontece e possivelmente precisa ser investigado. Você pode encontrar uma lista de possíveis erros com sua descrição na documentação


Em caso de erro, registramos esse fato e redirecionamos o usuário com segurança para a página de login


* handleError pode ser chamado com o erro INVALID_ARGUMENT após a atualização das chaves públicas do Google.

Esta é uma forma de rotação de chaves e é esperada . Veja esta edição do Github para mais informações


Agora é isso. Finalmente.


Vamos sair do nosso aplicativo da web e abrir http://localhost:3000/ . Devemos ser redirecionados para a página /login .

Vamos fazer login novamente e tentar inserir http://localhost:3000/login . Devemos ser redirecionados para / página.


Não apenas fornecemos uma experiência de usuário perfeita. next-firebase-auth-edge é uma biblioteca de tamanho zero que funciona apenas no servidor do aplicativo e não introduz código adicional do lado do cliente. O pacote resultante é verdadeiramente mínimo . Isso é o que eu chamo de perfeito.


Nosso aplicativo agora está totalmente integrado ao Firebase Authentication nos componentes Servidor e Cliente. Estamos prontos para liberar todo o potencial do Next.js!




O código-fonte do aplicativo pode ser encontrado em next-firebase-auth-edge/examples/next-typescript-minimal


Epílogo

Neste guia, abordamos a integração do novo aplicativo Next.js com Firebase Authentication.


Embora bastante extenso, o artigo omitiu algumas partes importantes do fluxo de autenticação, como formulário de redefinição de senha ou métodos de login diferentes de e-mail e senha.


Se estiver interessado na biblioteca, você pode visualizar a página de demonstração completa do next-firebase-auth-edge starter .

Possui integração com Firestore , ações de servidor , suporte para App-Check e muito mais


A biblioteca fornece uma página de documentação dedicada com vários exemplos


Se você gostou do artigo, eu apreciaria estrelar o repositório next-firebase-auth-edge . Saúde! 🎉



Bônus – Implantando aplicativo no Vercel

Este guia bônus ensinará como implantar seu aplicativo Next.js no Vercel

Criando repositório git

Para poder implantar no Vercel, você precisará criar um repositório para seu novo aplicativo.


Acesse https://github.com/ e crie um novo repositório.


create-next-app já iniciou um repositório git local para nós, então você só precisa seguir até a pasta raiz do seu projeto e executar:


 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


Adicionando novo projeto Vercel

Acesse https://vercel.com/ e faça login com sua conta Github


Depois de fazer login, vá para a página Visão geral do Vercel e clique em Adicionar novo> Projeto


Clique em Importar ao lado do repositório Github que acabamos de criar. Não implante ainda.


Antes de implantarmos, precisamos fornecer a configuração do projeto. Vamos adicionar algumas variáveis de ambiente:

Implantando no Vercel


Lembre-se de definir USE_SECURE_COOKIES como true , pois o Vercel usa HTTPS por padrão


Agora, estamos prontos para clicar em Implantar


Espere um ou dois minutos e você poderá acessar seu aplicativo com um URL semelhante a este: https://next-typescript-minimal-xi.vercel.app/


Feito. Aposto que você não esperava que fosse tão fácil.




Se você gostou do guia, eu apreciaria estrelar o repositório next-firebase-auth-edge .


Você também pode me deixar saber seu feedback nos comentários. Saúde! 🎉