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.
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.
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.
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.
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
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
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.
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
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.
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:
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
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 };
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
.
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!
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
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:
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'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
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:
/register
e /login
, devemos redirecioná-lo para /
/
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
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! 🎉
Este guia bônus ensinará como implantar seu aplicativo Next.js no Vercel
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
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:
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! 🎉