Capa do Ebook gratuito Roteamento e Autenticação em React: Construindo SPAs Seguras com React Router e JWT

Roteamento e Autenticação em React: Construindo SPAs Seguras com React Router e JWT

Novo curso

20 páginas

Interpretação e validações no cliente: JWT, claims e limites do front-end

Capítulo 9

Tempo estimado de leitura: 12 minutos

+ Exercício
Audio Icon

Ouça em áudio

0:00 / 0:00

Neste capítulo, o foco é entender o que o front-end consegue (e não consegue) fazer com um JWT: como interpretar o token, extrair claims, validar aspectos básicos no cliente e aplicar essas informações na UI e no roteamento. A ideia é usar o JWT como uma fonte de dados para experiência do usuário (ex.: mostrar nome, controlar menus, desabilitar ações), sem cair na armadilha de “confiar” no token para segurança real. Segurança de verdade depende de validações no servidor.

O que é um JWT na prática (para o front-end)

Um JSON Web Token (JWT) é uma string composta por três partes separadas por ponto: header.payload.signature. As duas primeiras partes são Base64URL (um formato de Base64 adaptado para URLs). A terceira parte é uma assinatura criptográfica (ou, em alguns casos, pode nem existir, como em tokens inseguros do tipo alg: none, que devem ser rejeitados).

  • Header: metadados do token, como o algoritmo de assinatura (alg) e o tipo (typ).
  • Payload: onde ficam as claims (declarações) sobre o usuário e o token.
  • Signature: garante integridade (se o payload for alterado, a assinatura não confere). Importante: o front-end normalmente não tem a chave para validar a assinatura de forma confiável.

No cliente, o JWT é útil principalmente para: (1) ler informações do usuário (claims), (2) verificar expiração e tomar decisões de UX, (3) anexar o token em requisições para APIs, (4) orientar a UI (ex.: permissões) de forma otimista, sempre assumindo que o servidor é a autoridade final.

Claims: padrão, customizadas e como interpretá-las

Claims são campos no payload do JWT. Existem claims registradas (padronizadas), públicas e privadas (customizadas). Algumas das mais comuns:

  • sub (subject): identificador do usuário (ex.: id).
  • iss (issuer): quem emitiu o token.
  • aud (audience): para quem o token é destinado.
  • exp (expiration time): timestamp (em segundos) de expiração.
  • iat (issued at): quando foi emitido.
  • nbf (not before): não é válido antes desse instante.
  • jti (JWT ID): id único do token (útil para revogação no servidor).

Claims customizadas variam por projeto: name, email, roles, permissions, tenantId, plan, features, etc. No front-end, essas claims podem alimentar:

Continue em nosso aplicativo

Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.

ou continue lendo abaixo...
Download App

Baixar o aplicativo

  • Exibição de dados do usuário no cabeçalho.
  • Controle de itens de menu (ex.: mostrar “Admin” apenas para roles adequadas).
  • Bloqueio de ações na UI (ex.: desabilitar botão “Excluir” se não tiver permissão).
  • Seleção de tenant/organização (multi-tenant) e escopo de dados.

Mesmo quando a UI usa claims para habilitar/desabilitar recursos, o servidor deve validar novamente. O front-end não é um ambiente confiável: o usuário pode alterar armazenamento, interceptar requests, modificar JS em runtime e forjar estados.

Decodificando JWT no cliente com segurança prática

Decodificar um JWT significa ler o payload. Isso não valida assinatura; apenas interpreta o conteúdo. Ainda assim, é útil para UX. Você pode decodificar manualmente ou usar uma biblioteca (ex.: jwt-decode). A abordagem com biblioteca reduz erros de Base64URL e parsing.

Passo a passo: decodificar e tipar claims

1) Instale uma biblioteca de decode (opcional, mas recomendado):

npm i jwt-decode

2) Defina um tipo para as claims que você espera. Evite assumir que tudo existe; trate como opcional e valide:

export type JwtClaims = {  sub?: string;  iss?: string;  aud?: string | string[];  exp?: number;  iat?: number;  nbf?: number;  jti?: string;  name?: string;  email?: string;  roles?: string[];  permissions?: string[];  tenantId?: string;};

3) Crie uma função utilitária para decodificar com tratamento de erro:

import { jwtDecode } from "jwt-decode";import type { JwtClaims } from "./JwtClaims";export function decodeJwt(token: string): JwtClaims | null {  try {    return jwtDecode<JwtClaims>(token);  } catch {    return null;  }}

4) Use a função para derivar estado de UI (ex.: nome e papéis). Se o token for inválido (malformado), trate como sessão inválida.

Validações que fazem sentido no front-end (e as que não fazem)

É comum querer “validar” o JWT no cliente. Mas é importante separar:

  • Validações de UX: ajudam a evitar telas quebradas, loops de navegação e chamadas desnecessárias.
  • Validações de segurança: devem ser feitas no servidor, porque o cliente não é confiável.

Validações úteis no cliente

  • Formato do token: tem três partes? é decodificável?
  • Expiração (exp): se expirou, não tente usar; redirecione para login ou acione refresh (se existir).
  • nbf e iat: podem ajudar a detectar tokens ainda não válidos (raro em SPAs, mas possível).
  • Presença de claims necessárias para UI: ex.: se sua UI depende de tenantId e ele não existe, trate como sessão inconsistente.
  • Validação superficial de iss/aud: pode evitar usar um token de outro ambiente (ex.: staging vs produção) por engano. Não é segurança real, mas reduz erros.

Validações que NÃO são confiáveis no cliente

  • Validar assinatura com segredo no front-end: impossível de forma segura, porque qualquer segredo embutido no bundle pode ser extraído.
  • Confiar em roles/permissions para autorizar ações sensíveis: o usuário pode trocar o token armazenado por outro ou manipular o estado. O servidor deve checar permissões em cada endpoint.
  • “Provar identidade” apenas com base no token decodificado: decodificar não prova que o token é legítimo.

Checagem de expiração e clock skew

exp é um timestamp em segundos (Unix epoch). No front-end, você normalmente compara com Date.now() (em milissegundos). Também é comum aplicar uma margem de segurança (clock skew) para evitar usar tokens quase expirados durante uma requisição.

Passo a passo: função para expiração com margem

import type { JwtClaims } from "./JwtClaims";export function isTokenExpired(claims: JwtClaims, skewSeconds = 30): boolean {  if (!claims.exp) return true;  const nowSeconds = Math.floor(Date.now() / 1000);  return nowSeconds >= (claims.exp - skewSeconds);}

Uso típico: se expirado (ou prestes a expirar), você evita disparar chamadas que certamente vão falhar com 401, e pode iniciar um fluxo de renovação (se seu backend suportar) ou redirecionar para login.

Observação: se o relógio do usuário estiver muito errado, qualquer validação temporal no cliente pode falhar. Por isso, trate isso como heurística de UX, não como regra de segurança.

Aplicando claims na UI: roles e permissions

Dois padrões comuns:

  • RBAC (Role-Based Access Control): o token traz roles (ex.: ["admin", "editor"]).
  • PBAC/ABAC: o token traz permissões específicas (permissions) ou atributos (ex.: department, plan).

No front-end, você pode criar helpers para checar permissões e condicionar renderização. Isso melhora a experiência (não mostrar botões que vão falhar), mas não substitui a autorização no servidor.

Passo a passo: helpers de autorização para UI

import type { JwtClaims } from "./JwtClaims";export function hasRole(claims: JwtClaims | null, role: string): boolean {  return !!claims?.roles?.includes(role);}export function hasPermission(claims: JwtClaims | null, perm: string): boolean {  return !!claims?.permissions?.includes(perm);}export function hasAnyPermission(claims: JwtClaims | null, perms: string[]): boolean {  const set = new Set(claims?.permissions ?? []);  return perms.some(p => set.has(p));}

Exemplo de uso em componente:

function DeleteButton({ claims }: { claims: JwtClaims | null }) {  const canDelete = hasPermission(claims, "orders:delete");  return (    <button disabled={!canDelete}>Excluir pedido</button>  );}

Mesmo com o botão desabilitado, o usuário pode chamar o endpoint manualmente. Por isso, o backend deve verificar orders:delete no endpoint de exclusão.

Validação de consistência: token, usuário e estado da aplicação

Um problema comum em SPAs é o estado ficar inconsistente: a UI acha que está autenticada, mas o token é inválido; ou o token existe, mas não contém claims necessárias; ou o token pertence a outro tenant e a UI está em um tenant diferente.

Uma estratégia prática no cliente é criar uma função de “sanidade” do token e claims, para decidir se a sessão é utilizável para a UI.

Passo a passo: validar sanidade do token decodificado

import type { JwtClaims } from "./JwtClaims";type TokenSanity = { ok: true; claims: JwtClaims } | { ok: false; reason: string };export function validateTokenSanity(claims: JwtClaims | null): TokenSanity {  if (!claims) return { ok: false, reason: "Token malformado" };  if (!claims.sub) return { ok: false, reason: "Claim sub ausente" };  if (!claims.exp) return { ok: false, reason: "Claim exp ausente" };  // Exemplo: sua aplicação exige tenantId  if (!claims.tenantId) return { ok: false, reason: "Claim tenantId ausente" };  return { ok: true, claims };}

Como usar: se ok: false, você pode limpar sessão local e forçar login. Isso evita que a UI tente operar com dados incompletos.

JWT não é um “banco de dados”: limites e cuidados com claims

É tentador colocar muitas informações no JWT para evitar chamadas ao servidor. Isso traz problemas:

  • Tamanho: JWT grande aumenta custo em cada request (especialmente se enviado em header Authorization). Pode afetar performance e bater limites de proxies/servidores.
  • Dados desatualizados: se o usuário muda de plano, permissões ou nome, o token antigo continua carregando dados antigos até expirar ou ser reemitido.
  • Exposição: JWT não é criptografado por padrão, apenas assinado. Qualquer pessoa com acesso ao token consegue ler o payload. Não coloque dados sensíveis (ex.: CPF, endereço, segredos, informações médicas).
  • Revogação: JWT é “auto-contido”; revogar antes do exp exige estratégia no servidor (lista de revogação, jti, rotação, etc.). O cliente não resolve isso sozinho.

Use claims para identificação e autorização de alto nível (ex.: sub, roles, tenantId) e mantenha dados detalhados no backend, consultando quando necessário.

Erros comuns ao interpretar JWT no front-end

  • Confundir Base64 com criptografia: o payload é facilmente decodificável.
  • Assumir que “decodificou, então é válido”: decodificar não valida assinatura nem garante que o token foi emitido por quem você espera.
  • Não tratar ausência de claims: tokens podem variar por ambiente, versão de backend ou tipo de usuário.
  • Não considerar expiração durante navegação: o usuário pode ficar com a aba aberta e o token expirar no meio do uso.
  • Usar claims para esconder rotas como se fosse segurança: esconder UI não impede acesso a endpoints.

Integração com chamadas HTTP: anexar token e reagir a 401/403

Mesmo que este capítulo não foque em armazenamento, a interpretação do JWT afeta como você reage a respostas do servidor:

  • 401 Unauthorized: normalmente indica token ausente, inválido ou expirado. No cliente, você pode limpar a sessão e redirecionar para login.
  • 403 Forbidden: token válido, mas sem permissão. No cliente, você pode mostrar uma tela “Sem acesso” e ajustar UI.

Uma prática útil é centralizar a leitura de claims e a checagem de expiração antes de disparar requests, para reduzir falhas previsíveis.

Passo a passo: checar expiração antes de request (exemplo genérico)

import { decodeJwt } from "./decodeJwt";import { isTokenExpired } from "./isTokenExpired";export async function authFetch(input: RequestInfo, init: RequestInit = {}) {  const token = localStorage.getItem("access_token");  if (!token) throw new Error("Sem token");  const claims = decodeJwt(token);  if (!claims || isTokenExpired(claims, 30)) {    throw new Error("Token expirado ou inválido");  }  const headers = new Headers(init.headers);  headers.set("Authorization", `Bearer ${token}`);  const res = await fetch(input, { ...init, headers });  if (res.status === 401) {    // aqui você pode disparar logout/limpeza de sessão    throw new Error("Não autorizado (401)");  }  if (res.status === 403) {    throw new Error("Proibido (403)");  }  return res;}

Esse padrão evita enviar token obviamente expirado, mas ainda depende do servidor para validar assinatura, revogação, permissões e escopo.

Quando faz sentido validar iss e aud no cliente

Em aplicações com múltiplos ambientes (dev/staging/prod) ou múltiplos emissores (ex.: migração de auth), pode acontecer de um token “válido” em um contexto ser usado no outro por engano. Validar iss e aud no cliente pode reduzir bugs e confusão do usuário.

Passo a passo: validação superficial de issuer/audience

import type { JwtClaims } from "./JwtClaims";type IssAudConfig = {  expectedIss?: string;  expectedAud?: string;};export function matchesIssAud(claims: JwtClaims, cfg: IssAudConfig): boolean {  if (cfg.expectedIss && claims.iss !== cfg.expectedIss) return false;  if (cfg.expectedAud) {    const aud = claims.aud;    if (Array.isArray(aud)) return aud.includes(cfg.expectedAud);    if (typeof aud === "string") return aud === cfg.expectedAud;    return false;  }  return true;}

Se não bater, trate como sessão inválida para aquele front-end e peça novo login. Reforçando: isso não substitui validação do servidor; é uma checagem de consistência.

Estratégias de UI para expiração: avisos, bloqueios e renovação

Quando o token está perto de expirar, você pode melhorar a experiência com estratégias de UI:

  • Aviso de sessão: exibir um banner “Sua sessão expira em X minutos”.
  • Bloqueio progressivo: desabilitar ações que exigem autenticação quando faltar pouco tempo (evita operações interrompidas).
  • Renovação: se existir mecanismo de refresh token no seu backend, o cliente pode tentar renovar antes de expirar. Se não existir, o caminho é redirecionar para login.

Mesmo sem implementar renovação aqui, você pode calcular “tempo restante” com base em exp e atualizar a UI.

Passo a passo: calcular tempo restante

import type { JwtClaims } from "./JwtClaims";export function getTokenTtlSeconds(claims: JwtClaims): number {  if (!claims.exp) return 0;  const nowSeconds = Math.floor(Date.now() / 1000);  return Math.max(0, claims.exp - nowSeconds);}

Com isso, você consegue renderizar um aviso quando o TTL ficar abaixo de um limiar (ex.: 120 segundos).

Checklist mental: o que o front-end deve assumir sobre JWT

  • O front-end pode ler claims para UX, mas não pode confiar nelas para segurança.
  • O front-end pode detectar expiração e evitar requests inúteis.
  • O front-end deve tratar token malformado/sem claims como sessão inválida.
  • O backend é quem valida assinatura, revogação, permissões e escopo em cada requisição.
  • Claims devem ser mínimas e não sensíveis, porque o payload é legível.

Agora responda o exercício sobre o conteúdo:

Qual é a forma correta de usar as claims de um JWT no front-end de uma SPA?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

No cliente, o JWT serve para ler claims e melhorar a experiência (menus, botões, navegação e checagem de expiração), mas isso não é segurança. O servidor deve validar assinatura, revogação e permissões em cada requisição.

Próximo capitúlo

Proteção por papéis (roles) e permissões por rota e por componente

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.