Skip to content
This page has been auto-translated and may contain errors.View in English

Autenticação

Um usuário autenticado atualiza a página e volta para a tela de login. As credenciais estavam corretas. O app renderizou sua proteção de rota antes de terminar de checar se uma sessão existia, e a proteção leu a resposta ausente como "desconectado".

Acertar isso começa com ser preciso sobre o que é uma sessão: um pedaço de estado que diz que alguém está autenticado e carrega sua identidade, armazenado num só lugar para que todo componente leia a mesma resposta. O curso constrói isso contra um projeto de CRM usando Supabase, mas a estrutura é a mesma com qualquer provedor de auth: Firebase, Auth0, Clerk, ou seu próprio backend. Este capítulo cobre essa estrutura.

O estado da sessão vive em um provider

Se um usuário está autenticado importa em todo lugar: o roteador precisa disso para proteger páginas, o header precisa para mostrar quem está conectado, formulários precisam para saber para onde enviar dados. Essa é exatamente a situação para a qual context existe, e o movimento padrão é o padrão provider-plus-hook daquele capítulo: um AuthProvider é dono do estado da sessão, e um hook useAuth o entrega a qualquer componente que o pedir.

O estado da sessão tem três valores significativos, e o terceiro é aquele que as pessoas perdem:

  • undefined: a verificação não terminou. O app carregou e ainda está perguntando ao provider se uma sessão existe.
  • null: a verificação voltou, e ninguém está autenticado.
  • um objeto de sessão: alguém está autenticado, e o objeto carrega sua identidade.

Esse terceiro valor é o que corrige o rebote de atualização do começo. A sessão ainda existe no storage do navegador, mas por um momento após o primeiro render o app ainda não a leu. Começando em undefined permite que o app renderize um estado de carregamento breve em vez de dar uma resposta errada.

Verificar uma sessão existente e ouvir mudanças ambos atingem fora do React, então pertencem a um efeito que roda uma vez após o primeiro render:

jsx
import { createContext, useContext, useState, useEffect } from 'react'
import { auth } from './authClient'

const AuthContext = createContext(null)

export function AuthProvider({ children }) {
  const [session, setSession] = useState(undefined)

  useEffect(() => {
    async function getInitialSession() {
      const { data } = await auth.getSession()
      setSession(data.session) // o objeto de sessão, ou null
    }
    getInitialSession()

    const {
      data: { subscription },
    } = auth.onAuthStateChange((_event, session) => {
      setSession(session)
    })
    return () => subscription.unsubscribe()
  }, [])

  return <AuthContext value={{ session }}>{children}</AuthContext>
}

export function useAuth() {
  const context = useContext(AuthContext)
  if (context === null) {
    throw new Error('useAuth must be used within an AuthProvider')
  }
  return context
}

Dois trabalhos acontecem naquele efeito. getSession faz a verificação única: existe uma sessão já sentada no storage de uma visita anterior? Depois onAuthStateChange configura um listener, algo como ligar uma câmera de segurança, que dispara sempre que o usuário faz login ou logout e mantém o estado atual a partir de então. Nomes de método variam por provider, mas todo SDK de auth oferece ambos os movimentos: ler a sessão atual uma vez, e se inscrever em mudanças.

Observe a ordem

Um getSession lento pode resolver depois que o listener já entregou uma sessão mais nova e sobrescrevê-la com a desatualizada. Muitos SDKs disparam o listener com a sessão inicial ao se inscrever, nesse caso definir estado somente a partir do listener remove a corrida.

O que é um JSON Web Token

Uma string viaja com cada requisição para provar quem está pedindo. Abra as devtools em um app autenticado, copie aquela string do storage, e pergunte o que a pessoa que a segura pode ver. A resposta é tudo. Atrás do objeto de sessão fica um JSON Web Token, três partes separadas por pontos: um header nomeando o algoritmo de assinatura, um payload de claims sobre o usuário, e uma assinatura. Cada parte é codificada em base64url, e codificação é um passo de formatação em vez de criptografia:

js
// um token é três segmentos base64url unidos por pontos
const [header, payload, signature] = token.split('.')
// atob lê base64 padrão, então troque os dois caracteres URL-safe primeiro
JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')))
// { sub: '9f2c...', email: '[email protected]', exp: 1789450000 }

Duas instruções num console do navegador, nenhuma chave envolvida. Então um payload de JWT é legível por qualquer um segurando o token, o que torna uma regra absoluta: segredos nunca pertencem a um claim. Claims padrão são coisas como sub para o ID do usuário, exp para a hora de expiração, e o que mais o provider escolher incluir.

A assinatura é a parte que um leitor não pode falsificar. O provider assina o header e o payload com uma chave de assinatura, e em cada requisição o servidor verifica aquela assinatura. Alguns providers usam um único segredo compartilhado para tanto assinar quanto verificar, é por isso que aquele segredo fica no servidor; outros assinam com uma chave privada e publicam uma chave pública correspondente que qualquer verificador pode usar.

De qualquer forma, uma assinatura válida prova que o token foi emitido aqui e não foi alterado desde então. Não diz nada sobre quem leu o conteúdo.

O token fica no navegador, em local storage ou um cookie, e a biblioteca do cliente o anexa a cada requisição automaticamente. Onde fica é uma decisão de segurança. Local storage é legível por qualquer JavaScript na página, então um bug XSS vira roubo de token; cookies httpOnly são invisíveis para scripts, o que para o roubo, embora um script na página ainda possa agir como o usuário enquanto rodando, e cookies trazem preocupações CSRF que precisam de sua própria mitigação.

Carregar o token em cada requisição é o que torna auth JWT stateless: o servidor mantém nenhuma lista de quem está autenticado no momento. O token assinado em si é a prova, então o usuário prova quem é em cada requisição sem reenviar uma senha.

Supabase camadas uma convenção em cima dessa anatomia, e a convenção é de Supabase em vez de parte da especificação JWT: o token de um usuário autenticado carrega um claim role lendo authenticated, enquanto uma requisição feita antes de qualquer um fazer login recebe o papel anon.

Essas requisições não autenticadas viajam numa chave de API de projeto que é enviada dentro do seu bundle de cliente: a chave herdada é chamada a chave anon e é ela mesma um JWT, e a atual é chamada a chave publicável, uma string sb_publishable_... que não é um token. Enviá-la é seguro apenas quando Row Level Security está habilitado em cada tabela que pode alcançar, e a próxima seção cobre essas políticas. Sem elas, qualquer um que abra devtools pode ler e escrever aquelas linhas.

Mantenha a chave de serviço fora do cliente

A contraparte da chave publicável, a chave de service-role ou secret, ignora cada política, então nunca deve alcançar o bundle de cliente ou uma var env com prefixo VITE_, já que essas são compiladas no bundle que você envia.

Cadastrar, fazer login, fazer logout

Os três fluxos compartilham uma forma: chamar o método SDK do provider, deixar o listener de auth atualizar o estado da sessão, e navegar. Cada função de auth tipicamente vive no mesmo arquivo provider que o estado da sessão, fica exposta através do valor context, e retorna um resultado simples que o componente que chama pode agir:

jsx
async function signIn(email, password) {
  const { data, error } = await auth.signInWithPassword({
    email: email.toLowerCase(),
    password,
  })
  if (error) return { success: false, error: error.message }
  return { success: true, data }
}

Em sucesso o provider emite um JWT, a biblioteca de cliente o armazena e constrói o objeto de sessão, e o listener onAuthStateChange atualiza o estado. O componente que chamou signIn só precisa verificar o resultado e navegar:

jsx
const { signIn } = useAuth()

async function handleSubmit(email, password) {
  const { success, error } = await signIn(email, password)
  if (success) navigate('/dashboard')
  else setError(error)
}

Os outros dois fluxos são variações. Cadastrar envia as credenciais do novo usuário ao provider, que as armazena e, com a maioria dos providers, autentica o usuário ao mesmo tempo, então a mesma lógica de navegação aplica. Fazer logout chama o método de logout do SDK, que limpa o token armazenado; o listener vê a mudança, define a sessão como null, e o app navega de volta para uma página pública.

Nada nos fluxos é específico de React. O trabalho do React é segurar o estado da sessão e re-renderizar quando muda.

Protegendo rotas, e quem realmente aplica acesso

Com estado de sessão em context, a rota de layout que requer auth do capítulo rotas protegidas vira um ternário. Os três valores de sessão ganham um ramo:

jsx
function AuthRequired() {
  const { session } = useAuth()

  if (session === undefined) return <p>Loading...</p>
  return session ? <Outlet /> : <Navigate to="/signin" replace />
}

Coloque as páginas privadas sob ela e visitantes não autenticados são redirecionados antes de qualquer uma renderizar. Aquele capítulo fez o ponto que uma proteção no cliente é um recurso de experiência do usuário; aqui está o outro lado. Controle de acesso real tem que viver no servidor, onde os usuários não conseguem alcançar.

O exemplo do curso de aplicação no servidor é Row Level Security, um recurso de banco de dados onde políticas de acesso se anexam às tabelas em si. Habilite e tudo é negado por padrão; então políticas concedem acesso baseado em claims na requisição's JWT, como role = 'authenticated' ou a coluna de proprietário da linha combinando o sub do token. A política é aplicada dentro do banco de dados em cada query, então se sustenta até mesmo por uma requisição que pula seu app React.

O que permite é exatamente o que a política diz, e uma requisição carregando nada além da chave publicável é uma requisição legítima, então uma política escrita como sempre-verdadeira protege nada: a regra tem que nomear o claim que verifica. A proteção de rota e a política funcionam como um par, e a política é o lado carregando o peso.

O desenho stateless é uma troca com dois lados. O servidor pula uma busca de sessão em cada requisição, que é o ganho. O custo é revogação: já que nenhuma lista de sessões ativas existe, um token roubado ou desconectado permanece criptograficamente válido até expirar.

Providers gerenciam isso com tokens de acesso de vida curta emparelhados com tokens de refresh de vida longa. A biblioteca de cliente esconde a movimentação: getSession verifica a expiração do token armazenado, e quando encontra um token expirado troca silenciosamente o token de refresh por um token de acesso fresco, expondo null apenas quando aquela troca falha. Revogação instantânea requer reintroduzir estado no servidor, uma denylist verificada por requisição, é por isso que expirações curtas são a resposta padrão.

Os dois perigos de token de antes têm um frame mais aguçado. Uma assinatura garante integridade enquanto deixa confidencialidade intocada: um verificador aprende que o token é inalterado e não aprende nada sobre quem mais o leu no caminho.

No armazenamento, SDKs no cliente comumente padrão para local storage e dependem da expiração de token de acesso curto para limitar o dano, o que é uma aposta que um token roubado fica desatualizado antes de valer muito. Mover para cookies httpOnly realoca o problema em vez de removê-lo, já que mitigação CSRF passa a ser sua para acertar.

A regra de aplicação generaliza além de bancos de dados. Row Level Security é uma implementação; o princípio é que toda decisão de confiança deve acontecer onde o atacante não tem acesso de escrita. O que quer que o cliente envie, incluindo o JWT em si, é entrada controlada pelo atacante até o servidor verificar a assinatura. Essa verificação é o único momento entrada não confiável vira identidade confiável, é por isso que a chave de assinatura é a joia da coroa: quem quer que a segure pode cunhar tokens válidos para qualquer usuário.

JunoA sessão é estado, o servidor é a lei Fazer login dá ao seu app uma sessão, e aquela sessão é estado React mantido num provider de context para que todo componente possa perguntar "alguém está autenticado?" Tem três respostas: ainda verificando, desconectado, e autenticado, e a resposta "ainda verificando" evita que usuários sejam expulsos para a página de login enquanto o app procura uma sessão existente.

Por trás de tudo está um token assinado que viaja com cada requisição para provar quem está pedindo, e qualquer um segurando aquele token pode ler o que está dentro dele, então mantenha segredos fora dele.

Proteções de rota redirecionam educadamente visitantes desconectados, mas os fechaduras reais ficam no servidor.

JunoA sessão é estado, o servidor é a lei Construa um AuthProvider com um hook useAuth, exatamente como um theme provider. Inicialize a sessão como undefined, então num efeito chame getSession para o caso de refresh e se inscreva em mudanças de auth para tudo depois.

Fazer login, cadastrar, e fazer logout todos seguem uma forma: chamar o SDK, deixar o listener atualizar estado, navegar no sucesso.

Ligue a sessão a uma rota protegida com um ramo de carregamento, e coloque as regras de acesso reais no servidor, com políticas como Row Level Security que verificam o JWT em cada query.

JunoA sessão é estado, o servidor é a lei Auth JWT troca revogação por statelessness: o token assinado é a sessão, então nada o expira cedo sem uma denylist no servidor, e tokens de acesso de vida curta mais rotação de refresh são a mitigação padrão. O payload é legível por qualquer um segurando o token, então a assinatura garante integridade enquanto segredos ficam fora de claims, e onde você armazena o token decide se XSS ou CSRF é seu problema.

Trate tudo que o cliente envia como não confiável até verificação de assinatura, e aplique autorização onde o atacante não consegue escrever: o servidor.

Próximo: TypeScript em React, onde props, estado, e componentes ganham tipos.