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...Baixar o aplicativo
- Exibição de dados do usuário no cabeçalho.
- Controle de itens de menu (ex.: mostrar “Admin” apenas para
rolesadequadas). - 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-decode2) 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). nbfeiat: 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
tenantIde 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
expexige 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.