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:
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:
// 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:
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:
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:
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.
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.
Próximo: TypeScript em React, onde props, estado, e componentes ganham tipos.

