Nos últimos anos, adicionar autenticação ao seu aplicativo deixou de ser algo obscuro e complicado e passou a ser algo para o qual você pode literalmente usar apenas uma API.

Não faltam exemplos de repositórios e tutoriais sobre como implementar esquemas de autenticação específicos no Next.js, mas há menos informações sobre o porquê da escolha de esquemas, ferramentas e compensações.

Este artigo irá orientar sobre o que considerar ao abordar a autenticação no Next.js, desde a escolha de um provedor até a construção de rotas para login e a decisão entre servidor e o lado do cliente.

Escolhendo um método/fornecedor de autenticação

Existem basicamente 1.000 maneiras de criar autenticação em seu aplicativo. Em vez de nos concentrarmos em provedores específicos (assunto para outro artigo no blog), vamos analisar os tipos de soluções de autenticação e alguns exemplos de cada uma. Em termos de implementação, o next-auth está se tornando rapidamente uma opção popular para integrar seu aplicativo Next.js a vários provedores, adicionar SSO, etc.

Banco de dados tradicional

Este método é tão simples quanto parece: você armazena nomes de usuário e senhas em um banco de dados relacional. Quando um usuário se cadastra pela primeira vez, você insere uma nova linha na tabela users com as informações fornecidas. Quando eles fazem login, você verifica as credenciais em comparação com o que está armazenado na tabela. Quando um usuário quer mudar sua senha, você atualiza o valor na tabela.

A autenticação tradicional com banco de dados é certamente o esquema de autenticação mais popular, considerando a totalidade dos aplicativos existentes e existe basicamente desde sempre. É altamente flexível, econômica e não te prende a nenhum fornecedor específico. Mas você precisa construí-la você mesmo e, em particular, se preocupar com a criptografia e garantir que essas preciosas senhas não caiam em mãos erradas.

Soluções de autenticação do seu provedor de banco de dados

Nos últimos anos (e, para crédito do Firebase, há mais de alguns anos), tornou-se relativamente padrão para os provedores de bancos de dados gerenciados oferecer algum tipo de solução de autenticação gerenciada. O Firebase, o Supabase e o AWS oferecem banco de dados gerenciado e autenticação gerenciada como serviço por meio de um conjunto de APIs que abstrai facilmente a criação de usuários e o gerenciamento de sessões (falaremos mais sobre isso adiante).

Para conectar um usuário com a autenticação do Supabase, você só precisa fazer o seguinte:

async function signInWithEmail() {
  const { data, error } = await supabase.auth.signInWithPassword({
    email: '[email protected]',
    password: 'example-password',
  })
}

Soluções de autenticação que não são do seu provedor de banco de dados

Talvez até mais comum do que a autenticação como um serviço do seu DBaaS (Database as a Service) seja a autenticação como um serviço como uma empresa ou produto inteiro. Auth0, que existe desde 2013 (agora propriedade da Okta), e adições recentes como Stytch têm priorizado a experiência do desenvolvedor e ganhado uma certa relevância no mercado.

Auth0 para autenticação
Auth0 para autenticação

Single Sign-On (SSO)

O SSO permite que você “terceirize” sua identidade para um provedor externo, que pode variar desde um fornecedor focado em segurança empresarial como a Okta até algo mais amplamente adotado como o Google ou o GitHub. O SSO do Google é onipresente no mundo do SaaS, e algumas ferramentas voltadas para desenvolvedores autenticam apenas via GitHub.

Seja qual for o provedor que você escolher, o SSO geralmente é um complemento para os outros tipos de autenticação acima e tem suas próprias idiossincrasias em relação à integração com plataformas externas (atenção: o SAML usa XML).

Certo, então qual deles é para mim?

Não há uma escolha “correta” aqui – o que é certo para o seu projeto depende de suas prioridades. Se você quiser fazer as coisas andarem rapidamente sem muita configuração inicial, terceirizar a autenticação faz sentido (até mesmo terceirizá-la completamente, inclusive a interface do usuário, para alguém como a Auth0). Se você prevê uma configuração mais complexa, faz sentido criar seu próprio backend de autenticação. E se você pretende dar suporte a clientes maiores, precisará adicionar o SSO em algum momento.

O Next.js é tão popular neste momento que a maioria desses provedores de autenticação tem documentos e guias de integração específicos para o Next.js.

Criando rotas para inscrição e login, e dicas para você ir além da autenticação

Alguns provedores de autenticação, como Auth0, na verdade, fornecem páginas web hospedadas inteiras para cadastro e login. Mas se você está construindo essas páginas do zero, eu acho útil criá-las no início do processo, já que você precisará delas para fornecer como redirecionamentos quando realmente implementar sua autenticação.

Portanto, faz sentido criar a estrutura para essas páginas e, depois, adicionar as solicitações ao backend posteriormente. A maneira mais direta de implementar a autenticação é ter duas dessas rotas:

  • Uma para registro
  • Outra para fazer login quando o usuário já tiver uma conta

Além do básico, você precisará cobrir casos extremos, como quando um usuário esquece a senha. Algumas equipes preferem ter o processo de redefinição de senha em uma rota separada, enquanto outras adicionam elementos dinâmicos de interface do usuário (UI) à página regular de login.

Uma boa página de inscrição pode não significar a diferença entre o sucesso e o fracasso, mas pequenos toques podem deixar uma boa impressão e, em geral, proporcionar uma experiência do usuário melhor. Aqui estão alguns exemplos de sites da web que deram um toque especial aos seus processos de autenticação.

1. Atualize sua barra de navegação se houver uma sessão ativa

O call to action na barra de navegação da Stripe muda com base em se você tem uma sessão autenticada ou não. Veja como fica o site de marketing se você não estiver autenticado. Observe o call to action para fazer login:

A página inicial do Stripe altera o CTA com base no fato de você estar autenticado.
A página inicial do Stripe altera o CTA com base no fato de você estar autenticado.

E aqui está a aparência se você estiver autenticado. Observe que o Call to Action muda para levar o usuário ao painel de controle em vez de fazer login:

Mudanças na página inicial do Stripe.
Mudanças na página inicial do Stripe.

Isso não muda fundamentalmente minha experiência com o Stripe, mas é bom.

Uma observação técnica interessante: há um bom motivo para que a maioria das empresas não faça com que a barra de navegação em seu site de marketing “dependa” da autenticação – isso significaria uma solicitação de API extra para verificar o estado autenticado em cada carregamento de página, a maioria dos quais são para visitantes que provavelmente não são.

2. Adicione algum conteúdo útil junto com o formulário de inscrição

Nos últimos anos, especialmente em SaaS, as empresas começaram a adicionar conteúdo à página de inscrição para “incentivar” o usuário a realmente concluir a inscrição. Isso pode ajudar a melhorar sua conversão na página, pelo menos de forma incremental.

Aqui está uma página de inscrição da Retool, com uma animação e alguns logos na lateral:

Se você for fazer isso, tente garantir que as fontes de cada lado sejam iguais.
Se você for fazer isso, tente garantir que as fontes de cada lado sejam iguais.

Também fazemos isso na Kinsta para nossa página de inscrição:

Página de registro da Kinsta
Página de registro da Kinsta

O conteúdo extra pode ajudar a lembrar o usuário do motivo pelo qual ele está assinando e por que ele precisa disso.

3. Se você estiver usando uma senha: sugira ou imponha uma senha forte

Sinto-me à vontade para dizer que é de conhecimento comum entre os desenvolvedores que as senhas são inerentemente inseguras, mas não é de conhecimento comum entre todas as pessoas que se inscreverão no seu produto. Incentivar seus usuários a criar senhas seguras é bom para você e bom para eles.

A Coinbase é bastante rigorosa com a inscrição e exige que você use uma senha segura que seja mais complicada do que apenas seu primeiro nome:

Senha fraca na Coinbase.
Senha fraca na Coinbase.

Depois de gerar uma senha com meu gerenciador de senhas, eu estava pronto para começar:

Senha forte na Coinbase.
Senha forte na Coinbase.

A interface do usuário não me informou por que a senha não era segura o suficiente, nem quaisquer requisitos além de ter um número. Incluir essas informações no texto do seu produto tornará as coisas mais fáceis para o usuário e ajudará a evitar a frustração de ter que tentar outra senha.

4. Rotule suas entradas para que elas funcionem bem com um gerenciador de senhas

Um em cada três americanos usa um gerenciador de senhas como o 1Password e, mesmo assim, muitos formulários na web continuam a ignorar o `type=` nas entradas HTML. Faça com que seus formulários sejam compatíveis com os gerenciadores de senhas:

  • Coloque seus elementos de entrada em um elemento de formulário
  • Dê às entradas um tipo e um rótulo
  • Adicione recursos de preenchimento automático às suas entradas
  • Não adicione campos dinamicamente (estou olhando para você, Delta)

Isso pode fazer a diferença entre um login de 10 segundos incrivelmente fácil e um login manual irritante, especialmente em dispositivos móveis.

Escolhendo entre sessões e JWT

Depois que o usuário se autentica, você precisa escolher uma estratégia para manter esse estado nas solicitações subsequentes. O HTTP não tem estado e certamente não queremos pedir a senha do usuário em cada solicitação. Há dois métodos populares para lidar com issosessões (ou cookies) e JWTs (tokens da Web JSON) – e eles diferem em termos se o servidor ou o cliente faz o trabalho.

Sessões, também conhecidas como cookies

Na autenticação baseada em sessão, a lógica e o trabalho para manter a autenticação são tratados pelo servidor. Aqui está o fluxo básico:

  1. O usuário se autentica por meio da página de login.
  2. O servidor cria um registro que representa essa “sessão” de navegação específica Normalmente, esse registro é inserido em um banco de dados com um identificador aleatório e detalhes sobre a sessão, como quando ela começou e quando expira.
  3. Esse identificador aleatório – algo como `6982e583b1874abf9078e1d1dd5442f1` – é enviado ao seu navegador e armazenado como um cookie.
  4. Nas solicitações subsequentes do cliente, o identificador está incluído  e verificado na tabela de sessões do banco de dados.

É bastante simples e pode ser ajustado em relação ao tempo de duração das sessões, quando elas devem ser revogadas, etc. A desvantagem é a latência em uma escala significativa, considerando todas as gravações e leituras no banco de dados, mas isso pode não ser uma consideração importante para a maioria dos leitores.

Tokens da Web JSON (JWT)

Em vez de lidar com a autenticação para solicitações subsequentes no servidor, os JWTs permitem que você lide com elas (principalmente) no lado do cliente. Veja como funciona:

  1. O usuário se autentica por meio da página de login.
  2. O servidor gera um JWT que contém a identidade do usuário, as permissões que lhe foram concedidas e uma data de validade (entre outras coisas).
  3. O servidor assina esse token, criptografa o conteúdo dele e envia tudo para o cliente.
  4. Para cada solicitação, o cliente pode descriptografar o token e verificar se o usuário tem permissão para fazer a solicitação (tudo sem precisar se comunicar com o servidor).

Com todo o trabalho de autenticação pós-inicial transferido para o cliente, seu aplicativo pode ser carregado e funcionar muito mais rápido. Mas há um problema principal: não há como invalidar um JWT do servidor. Se o usuário quiser fazer logout de um dispositivo ou se o escopo da autorização mudar, você precisará esperar até que o JWT expire.

Escolhendo entre a autenticação do lado do servidor e do lado do cliente

Parte do que torna o Next.js excelente é a renderização estática integrada – se a sua página for estática, ou seja, não precisar fazer nenhuma chamada de API externa, o Next.js a armazenará automaticamente em cache e poderá servi-la com extrema rapidez por meio de uma CDN. A versão anterior ao Next.js 13 sabe se uma página é estática se você não incluir nenhum `getServerSideProps` ou `getInitialProps` no arquivo, enquanto as versões posteriores ao Next.js 13 usam o React Server Components para fazer isso.

Para autenticação, você tem uma opção: renderizar uma página estática de “carregamento” e fazer a busca no lado do cliente ou fazer tudo no lado do servidor. Para páginas que exigem autenticação [1], você pode renderizar um “skeleton” estático e, em seguida, fazer solicitações de autenticação no lado do cliente. Em teoria, isso significa que a página é carregada mais rapidamente, mesmo que o conteúdo inicial não esteja totalmente pronto para ser usado.

Aqui está um exemplo simplificado da documentação que renderiza um estado de carregamento, desde que o objeto do usuário não esteja pronto:

import useUser from '../lib/useUser'
 
const Profile = () => {
  // Fetch the user client-side
  const { user } = useUser({ redirectTo: '/login' })
 
  // Server-render loading state
  if (!user || user.isLoggedIn === false) {
    // Build some sort of loading page here
    return <div>Loading...</div>
  }
 
  // Once the user request finishes, show the user
  return (
    <div>
      <h1>Your Account</h1>
      <p>Username: {JSON.stringify(user.username,null)}</p>
      <p>Email: {JSON.stringify(user.email,null)}</p>
      <p>Address: {JSON.stringify(user.address,null)}</p>
    </div>
  )
}
 
export default Profile

Observe que você precisaria criar algum tipo de interface do usuário (UI) de carregamento aqui para manter o espaço enquanto o cliente faz solicitações após o carregamento.

Se quiser simplificar as coisas e executar a autenticação no lado do servidor, você poderá adicionar sua solicitação de autenticação à função `getServerSideProps`, e o Next aguardará para renderizar a página até que a solicitação seja concluída. Em vez da lógica condicional no snippet acima, você executaria algo mais simples, como esta versão simplificada dos documentos do Next:

import withSession from '../lib/session'
 
export const getServerSideProps = withSession(async function ({ req, res }) {
  const { user } = req.session
 
  if (!user) {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
      },
    }
  }
 
  return {
    props: { user },
  }
})
 
const Profile = ({ user }) => {
  // Show the user. No loading state is required
  return (
    <div>
      <h1>Your Account</h1>
      <p>Username: {JSON.stringify(user.username,null)}</p>
      <p>Email: {JSON.stringify(user.email,null)}</p>
      <p>Address: {JSON.stringify(user.address,null)}</p>
    </div>
  )
}
 
export default Profile

A lógica presente ainda trata casos de falha na autenticação, mas redireciona para a tela de login ao invés de exibir um estado de carregamento.

Resumo

Então, qual dessas opções é a certa para o seu projeto? Comece avaliando o grau de confiança que você tem na velocidade do seu esquema de autenticação. Se as solicitações não estiverem demorando nada, você poderá executá-las no lado do servidor e evitar o estado de carregamento. Se você quiser priorizar a renderização imediata de algo e depois aguardar a solicitação, ignore o `getServerSideProps` e execute a autenticação em outro lugar.

[1] Quando você usa o Next, esse é um bom motivo para não exigir a autenticação em todas as páginas. É mais simples fazer isso, mas significa que você perde os benefícios de desempenho da estrutura da web em primeiro lugar.

Justin Gage

Justin is a technical writer and author of the popular Technically newsletter. He did his B.S. in Data Science before a stint in full-stack engineering and now focuses on making complex technical concepts accessible to everyone.