Authentication

JWT

Um JSON Web Token é uma string assinada e segura para URL que carrega claims sobre um usuário. Compacto e autocontido, ele escala perfeitamente, mas é fácil de implementar incorretamente.

intermediate15 min readUpdated 16 de set. de 2026
verify.ts
ts
// verify.ts
import { jwtVerify, type JWTPayload } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

export async function verify(token: string): Promise<JWTPayload> {
  const { payload } = await jwtVerify(token, secret, {
    issuer: "https://auth.example.com",
    audience: "api.example.com",
    algorithms: ["HS256"],
  });
  return payload;
}
Primeira spec
2015
Encoding
Base64url
Algoritmo padrão
HS256
Opção assimétrica
RS256 / ES256
Variante criptografada
JWE

Por que importa

O que um JWT oferece

Credenciais autocontidas

Cada claim que um serviço precisa viaja dentro do token, portanto a validação é uma operação local, sem a necessidade de idas e vindas ao banco de dados no caminho crítico.

Evidência de adulteração por design

A assinatura cobre o header e o payload, então alterar um único caractere invalida o token e a verificação falha imediatamente.

Ajuste natural para rotação

Combinar access tokens de vida curta com refresh tokens rotativos limita o dano de um vazamento e oferece um caminho para a revogação.

O panorama completo

As três camadas de um token

Um header nomeia o algoritmo, um payload carrega as claims e uma assinatura sela ambos contra adulterações.

Header

Declarar

Um pequeno objeto JSON nomeando o algoritmo de assinatura e o ID da chave, para que os verificadores possam localizar a chave correta durante a rotação.

Payload

Carregar

Um objeto JSON de claims registradas e customizadas, como sub, exp, iss, aud, scope e role.

Signature

Selar

Um MAC criptográfico ou assinatura sobre o header e payload codificados que torna o token evidente contra adulterações.

HTML5 de uma olhada

Por dentro do token

Header

alg, typ e kid descrevem como o token foi assinado.

Claims

Nomes registrados como iss, sub, aud e exp, além de seus próprios campos.

Signature

Saída HMAC ou RSA/ECDSA que a verificação recomputa e compara.

Expiry

A claim exp limita a janela de tempo em que um token roubado é útil.

Rotation

Refresh tokens são de uso único e substituídos a cada troca.

Verification

Fixe o algoritmo e verifique iss, aud e exp antes de confiar em qualquer coisa.

Fluxo

Como um JWT é verificado

Toda requisição protegida segue o mesmo caminho. Qualquer verificação falha resulta em um 401.

  1. 1

    Extrair o token

    Leia o header Authorization e exija o esquema Bearer. Rejeite a requisição se nenhum token estiver presente.

  2. 2

    Dividir o token

    Quebre a string nos pontos em header, payload e assinatura. Um token sem exatamente três partes está malformado.

  3. 3

    Verificar a assinatura

    Recompute a assinatura sobre as duas primeiras partes com a chave e o algoritmo fixado. Uma divergência significa que o token foi adulterado.

  4. 4

    Validar as claims

    Verifique exp e nbf para o tempo, iss para o emissor esperado e aud para o público pretendido. Um token de staging não deve passar em produção.

  5. 5

    Anexar o principal

    Em caso de sucesso, coloque o subject e o scope na requisição para que os handlers seguintes possam autorizar sem re-analisar o token.

  6. 6

    Rejeitar com 401

    Se qualquer etapa falhar, retorne 401 com um erro genérico. Não explique qual verificação falhou para um chamador não confiável.

Uma breve historia

Como o JWT se tornou o padrão

  1. 2011

    O rascunho do JWT aparece

    O grupo de trabalho do OAuth propõe um formato de token compacto para carregar claims entre serviços.

    11
  2. 2015

    RFC 7519 padroniza o JWT

    O JWT é publicado junto com JWS, JWE, JWK e JWA, dando ao formato uma especificação estável.

    15
  3. 2015

    OpenID Connect o adota

    ID tokens são definidos como JWTs, tornando o formato o padrão para provedores de identidade.

    15
  4. 2015

    Surgem os ataques clássicos

    Pesquisadores documentam "alg: none" e a confusão HS/RS, forçando os verificadores a fixarem os algoritmos.

    15
  5. 2020

    A rotação torna-se a norma

    Access tokens curtos e refresh tokens rotativos substituem tokens de longa duração na prática comum.

    20

O guia completo

JWT: Tudo que voce precisa saber

O que é realmente um JWT

Um JSON Web Token é uma string compacta e segura para URLs que codifica um conjunto de claims junto com uma assinatura sobre elas. Ele é definido pela RFC 7519 e construído sobre duas especificações complementares: JSON Web Signature (JWS) para assinatura e JSON Web Encryption (JWE) para confidencialidade. Quase todo JWT que você encontrará na prática é um JWS.

O token é composto por três partes codificadas em Base64url e separadas por pontos: header, payload e assinatura. Ele é self-contained, o que significa que tudo o que um servidor precisa para tomar uma decisão viaja junto com a requisição. Validá-lo não requer consultas ao banco de dados, nem armazenamento de sessão compartilhado e nem chamadas ao emissor.

Anatomy of a JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzQyIiwic2NvcGUiOiJyZWFkOnBvc3RzIiwiZXhwIjoxNzYwMDAwMDAwfQ.3vJ1o0Q8m4nQ7bKc9xP2sT6wYqL0dRfH8uZgWmA1eJk
headerthe algorithm and token type, Base64url
payloadthe claims, also Base64url and readable by anyone
signatureHMAC over the first two parts, proving they were not changed

Vale a pena ser preciso sobre o que isso prova. Um JWT demonstra que quem o emitiu assinou exatamente esse payload. Ele não prova que o token foi destinado a você, a menos que você verifique a audience. Ele não prova que o usuário ainda tem permissão para agir, a menos que você verifique scopes e roles. E ele não esconde nada, a menos que você use JWE. Cada propriedade de segurança que for importante para você deve ser verificada explicitamente.

As três partes, decodificadas

O header é um pequeno objeto JSON que nomeia o algoritmo de assinatura e o tipo do token. O campo alg é o mais crítico. typ é quase sempre JWT, e kid identifica qual chave foi utilizada para que os verificadores possam encontrar a correta durante a rotação de chaves.

{ "alg": "HS256", "typ": "JWT", "kid": "2026-09" }

O payload é um objeto JSON de claims. As registered claims possuem nomes padronizados definidos pela especificação: iss para o emissor (issuer), sub para o assunto (subject), aud para a audiência (audience), exp para a expiração, nbf para “não antes de” (not before), iat para a data de emissão (issued at) e jti para um ID único do token. Todo o restante são custom claims que você mesmo define.

{
  "iss": "https://auth.example.com",
  "sub": "user_42",
  "aud": "api.example.com",
  "exp": 1760000000,
  "iat": 1759999100,
  "scope": "read:posts write:posts",
  "role": "editor"
}

A signature é computada sobre o Base64url do header e do payload. Altere um único caractere em qualquer um deles e a assinatura recomputada não coincidirá. Esta é a propriedade que torna um JWT seguro para ser entregue a um cliente não confiável, e é a única coisa que separa uma claim de uma falsificação.

Assinado, não criptografado

O mal-entendido mais comum sobre JWTs é que eles são secretos. Eles não são. O payload está em Base64url, que é uma codificação, não criptografia. Qualquer pessoa com o token pode decodificar cada claim com apenas uma linha de código.

const [, payload] = token.split(".");
console.log(JSON.parse(atob(payload)));
// { sub: "user_42", role: "editor", scope: "read:posts" }

Isso traz duas consequências. Primeiro, nunca coloque segredos, senhas ou dados pessoais em um JWT que você não se sentiria confortável em mostrar ao cliente. Segundo, trate o próprio token como uma credencial: a posse é suficiente para agir como o sujeito, e é por isso que o armazenamento e o transporte são tão importantes mais adiante neste guia.

Se você realmente precisar esconder o payload do cliente, use JSON Web Encryption e uma biblioteca que o suporte. Para a vasta maioria dos sistemas, um JWS assinado via TLS é a resposta correta, e adicionar criptografia apenas adicionaria mais uma complexidade desnecessária.

Algoritmos de assinatura: HS256 vs RS256

Duas famílias de algoritmos dominam as implementações reais.

HS256 é HMAC com SHA-256. O mesmo segredo assina e verifica. É rápido, simples e ideal quando um único serviço emite e verifica seus próprios tokens.

import { SignJWT, jwtVerify } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

const token = await new SignJWT({ role: "editor" })
  .setProtectedHeader({ alg: "HS256" })
  .setSubject("user_42")
  .setExpirationTime("15m")
  .sign(secret);

await jwtVerify(token, secret, { algorithms: ["HS256"] });

RS256 é RSA com SHA-256, e ES256 é ECDSA sobre uma curva prima. O emissor detém uma chave privada e assina; todos os outros detêm a chave pública e verificam. Essa assimetria é a razão pela qual sistemas grandes o preferem: um servidor de recursos pode verificar tokens sem ser capaz de emiti-los. O ES256 oferece a mesma garantia com chaves e assinaturas muito menores.

import { importPKCS8, importSPKI, SignJWT, jwtVerify } from "jose";

const privateKey = await importPKCS8(process.env.JWT_PRIVATE_KEY!, "RS256");
const publicKey = await importSPKI(process.env.JWT_PUBLIC_KEY!, "RS256");

const token = await new SignJWT({ role: "editor" })
  .setProtectedHeader({ alg: "RS256", kid: "2026-09" })
  .setSubject("user_42")
  .setExpirationTime("15m")
  .sign(privateKey);

await jwtVerify(token, publicKey, {
  algorithms: ["RS256"],
  issuer: "https://auth.example.com",
  audience: "api.example.com",
});

A regra que importa mais do que a escolha: o verificador deve fixar o algoritmo esperado. Nunca permita que o próprio header do token selecione como ele deve ser verificado.

Claims: registradas e customizadas

As registered claims são reservadas pela especificação e compreendidas por qualquer biblioteca séria.

  • iss — quem emitiu o token. Verifique isso para rejeitar tokens de outro ambiente.
  • sub — sobre quem é o token, geralmente o id do usuário.
  • aud — para quem é o token. Um token emitido para sua API pública não deve ser aceito por sua API de administração.
  • exp — quando o token expira, como um Unix timestamp. Sempre defina este valor.
  • nbf — não antes (not before). Raramente necessário, mas útil para rollouts graduais.
  • iat — quando foi emitido. Útil para verificações de idade máxima e debugging.
  • jti — um id único para este token, usado para detecção de replay e deny-lists.

As custom claims carregam dados da aplicação: scope, role, tenant_id, email. Mantenha-as pequenas e sem informações sensíveis. Um token viaja em cada requisição, portanto, um payload inflado é um custo permanente em banda e latência.

Existe uma forte tentação de colocar todo o perfil do usuário no token para evitar uma leitura no banco de dados. Resista a isso. As claims ficam obsoletas no momento em que uma role muda, e você não pode “desemitir” um token que o cliente já possui. Coloque apenas o que o verificador genuinamente precisa e busque o restante no banco.

Criando e verificando tokens

No Node, jose é a escolha moderna. Ele é baseado em promises, funciona em qualquer runtime, incluindo Cloudflare Workers e Deno, e expõe uma API concisa e cuidadosa. jsonwebtoken é a biblioteca mais antiga, baseada em callbacks, e continua sendo comum em códigos legados.

import { SignJWT } from "jose";

export async function issueAccessToken(userId: string, scope: string) {
  return new SignJWT({ scope })
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setSubject(userId)
    .setIssuer("https://auth.example.com")
    .setAudience("api.example.com")
    .setIssuedAt()
    .setExpirationTime("15m")
    .setJti(crypto.randomUUID())
    .sign(new TextEncoder().encode(process.env.JWT_SECRET));
}

A verificação é onde a segurança reside. Um verificador deve checar a assinatura, fixar o algoritmo e validar exp, iss e aud. Uma biblioteca pode decodificar um token sem verificá-lo tranquilamente, e esse payload decodificado é um input controlado por um atacante.

import { jwtVerify, type JWTPayload } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

export async function verifyAccessToken(token: string): Promise<JWTPayload> {
  const { payload } = await jwtVerify(token, secret, {
    algorithms: ["HS256"],
    issuer: "https://auth.example.com",
    audience: "api.example.com",
  });
  return payload;
}

Observe que jwtVerify impõe exp e nbf automaticamente, mas só verifica iss e aud quando você os passa. Omiti-los é uma vulnerabilidade real: um token emitido para um serviço de staging seria aceito em produção, e um token emitido para uma aplicação diferente também seria aceito.

Access tokens e refresh tokens

Um único token de longa duração é conveniente, porém perigoso. A solução padrão é utilizar um par com tempos de vida e públicos diferentes.

  • O access token tem vida curta, tipicamente de 5 a 15 minutos. Ele é enviado em cada chamada de API e é o único token que o servidor de recursos visualiza.
  • O refresh token tem vida longa, variando de dias a meses. Ele é enviado apenas ao servidor de autorização, em troca de um novo access token.

Essa divisão limita os danos. Se um access token vazar, ele se tornará inútil em poucos minutos. O refresh token, que é muito mais sensível, nunca viaja para os servidores de recursos e pode ser rotacionado e revogado, que é onde reside o controle real.

Rotação de refresh token

A rotação significa que cada renovação emite um novo refresh token e invalida o anterior. Se um invasor roubar um refresh token e utilizá-lo, o cliente legítimo apresentará posteriormente um token já utilizado, permitindo que o servidor detecte o reuso e revogue toda a família de tokens.

import { randomUUID } from "node:crypto";

export async function rotateRefreshToken(presented: string) {
  const stored = await db.refreshToken.findUnique({ where: { token: presented } });

  if (!stored || stored.revokedAt || stored.expiresAt < new Date()) {
    if (stored) await revokeFamily(stored.familyId);
    throw new Error("invalid_refresh_token");
  }

  await db.refreshToken.update({
    where: { id: stored.id },
    data: { revokedAt: new Date(), replacedBy: randomUUID() },
  });

  return issueTokenPair(stored.userId, stored.familyId);
}

Armazene os refresh tokens com hash, exatamente como você faria com senhas. Um vazamento de banco de dados não deve entregar credenciais válidas a um invasor, e um refresh token nada mais é do que uma senha que ignora o formulário de login.

Onde armazenar tokens no navegador

Não existe um lugar perfeito, apenas trade-offs entre cross-site scripting e cross-site request forgery.

  • localStorage pode ser lido por qualquer script na página. Um único bug de XSS e o atacante exfiltra todos os tokens. Este é o erro que continua aparecendo em relatórios de violações de segurança.
  • In-memory, como uma variável em um módulo, é seguro contra XSS persistente, mas é perdido ao atualizar a página; por isso, geralmente é combinado com um refresh token em um cookie httpOnly.
  • Um cookie httpOnly, Secure e SameSite não pode ser lido por JavaScript, o que neutraliza o roubo de tokens via XSS. No entanto, isso reintroduz o CSRF, que é mitigado por SameSite=Lax ou Strict somado a um token CSRF.

Para uma aplicação de navegador, o padrão pragmático é um access token de curta duração mantido em memória e um refresh token rotativo em um cookie httpOnly com escopo limitado ao endpoint de refresh. Clientes nativos e server-side não possuem essa restrição e podem manter tokens em armazenamento seguro ou simplesmente em memória.

O trade-off da ausência de estado e a revogação

O grande atrativo dos JWTs é a ausência de estado (statelessness). Qualquer servidor pode verificar um token sem a necessidade de um estado compartilhado, o que escala perfeitamente entre regiões e torna o escalonamento horizontal trivial. O custo disso é que a revogação é genuinamente difícil. Um token assinado é válido até expirar, independentemente de você ter deletado o usuário, alterado a função dele ou feito o logout. Não existe um registro central para deletar.

Você pode recuperar parte desse controle:

  • Mantenha os access tokens com tempo de vida curto, para que a janela de revogação seja de minutos, e não de dias.
  • Mantenha uma deny-list de valores jti para casos raros de logout imediato, verificando-a a cada requisição. Isso reintroduz o estado, portanto, mantenha-a pequena e com expiração.
  • Adicione um claim de token_version por usuário e rejeite tokens cuja versão esteja obsoleta. Alterar a senha ou a função do usuário incrementa esse valor.

Este é o resumo honesto: JWTs trocam a facilidade de revogação pela facilidade de escalonamento. Se você precisa de revogação instantânea em todos os lugares, cookies de sessão apoiados por um store podem ser mais adequados, como abordado em Session Auth.

Confusão de algoritmos e alg none

Dois ataques são antigos o suficiente para estarem nos livros didáticos e ainda assim encontrarem vítimas.

O primeiro é o alg: none. Um atacante edita o header para {"alg":"none"} e remove a assinatura. Um verificador ingênuo que confia no header aceita o token forjado. O segundo é a confusão HS/RS. Um serviço que espera tokens RS256 é enganado para aceitar um token HS256 assinado com a chave pública RSA, que é pública por definição.

Ambos possuem a mesma solução: o verificador decide o algoritmo, não o token.

// Good: the verifier decides.
await jwtVerify(token, secret, { algorithms: ["HS256"] });

// Bad: the token decides.
const { header } = decodeProtectedHeader(token);
await jwtVerify(token, secret, { algorithms: [header.alg] });

Rejeite alg: none sumariamente, nunca derive a chave de uma fonte não confiável e trate o header como dados, não como instruções.

Escopos e autorização

A autenticação responde quem é o usuário, e os escopos respondem o que ele pode fazer. Um claim de scope é uma lista de permissões delimitada por espaços, e o middleware a verifica antes que um handler seja executado.

export function requireScope(required: string) {
  return (req, res, next) => {
    const granted = String(req.user.scope ?? "").split(" ");
    if (!granted.includes(required)) {
      return res.status(403).json({ error: "insufficient_scope" });
    }
    next();
  };
}

Mantenha os escopos abrangentes e estáveis, e aplique-os no servidor. Um token sem o escopo correto deve falhar com 403, não 401: o chamador está autenticado, mas não tem permissão. Para modelos de acesso mais complexos baseados em funções e atributos, consulte RBAC.

JWT vs tokens opacos

Um JWT é um bearer token que carrega sua própria validação. Um token opaco é uma string aleatória sem significado intrínseco, portanto, o servidor precisa consultá-lo para obter qualquer informação.

Tokens opacos levam vantagem em revogação e privacidade. Você pode deletar a sessão instantaneamente, e o token não revela nada caso vaze. O custo disso é uma ida e volta ao banco de dados ou cache em cada requisição. Já os JWTs vencem em escala e independência. Os serviços fazem a verificação localmente e não precisam de um armazenamento compartilhado, ao custo de haver uma janela de tempo para a revogação.

Muitos sistemas em produção utilizam ambos: um access token em JWT para velocidade e um refresh token opaco para controle. Esse modelo híbrido é o que a maioria dos provedores de identidade entrega hoje em dia, e é uma ótima escolha padrão quando você está em dúvida.

Rotação de chaves com kid

Chaves de assinatura não devem durar para sempre. A rotação limita os danos de uma chave comprometida, e o campo de cabeçalho kid é o que torna a rotação invisível para os clientes: o verificador lê o kid, seleciona a chave correspondente e valida a assinatura.

import { SignJWT, jwtVerify, createLocalJWKSet } from "jose";

const jwks = createLocalJWKSet({
  keys: [{ kty: "oct", kid: "2026-09", k: process.env.JWT_SECRET }],
});

// The verifier resolves the key from the header's kid.
await jwtVerify(token, jwks, { algorithms: ["HS256"] });

Ao realizar a rotação, publique a nova chave ao lado da antiga, assine novos tokens com o novo kid e continue verificando a chave antiga até que todos os tokens assinados com ela tenham expirado. Se removê-la cedo demais, você desconectará todos os usuários ativos de uma só vez. Com chaves assimétricas, publique um documento JWKS em uma URL well-known e permita que os servidores de recurso façam o cache dele.

Tempo de vida de tokens na prática

A expiração é um equilíbrio entre segurança e conveniência. Não existe uma resposta universal correta, mas a estrutura de um padrão sensato é consistente.

  • Access tokens: 5 a 15 minutos. Curto o suficiente para que um vazamento se torne inútil rapidamente, e longo o suficiente para que você não precise fazer o refresh em cada requisição.
  • Refresh tokens: 7 a 30 dias, com rotação e sliding window. Longo o suficiente para manter os usuários conectados, mas curto o suficiente para que um token abandonado eventualmente expire.
  • Limite absoluto de sessão: 30 a 90 dias. Uma idade máxima após a qual o usuário deve se autenticar novamente, independentemente de quantas vezes ele tenha feito o refresh.

Se os seus usuários reclamarem de serem desconectados, a solução é um silent refresh mais fluido, e não um access token mais longo. Um access token de 24 horas é um problema de revogação esperando para acontecer.

Depurando um token sem confiar nele

Quando uma requisição falha com 401, você quer ver o que o token afirma sem comprometer a verificação. A decodificação é segura, desde que você trate o resultado como dados não confiáveis.

import { decodeJwt, decodeProtectedHeader } from "jose";

const header = decodeProtectedHeader(token);
const claims = decodeJwt(token);

console.log({ alg: header.alg, kid: header.kid });
console.log({
  sub: claims.sub,
  iss: claims.iss,
  aud: claims.aud,
  exp: new Date((claims.exp ?? 0) * 1000).toISOString(),
  expired: (claims.exp ?? 0) * 1000 < Date.now(),
});

As duas falhas que você mais verá são uma divergência de aud após a renomeação de um serviço e uma divergência de iss após a mudança entre ambientes. Ambos são problemas de configuração e ambos são invisíveis até que você imprima as claims.

Testando tokens

Você deve ser capaz de testar uma rota autenticada sem precisar subir um provedor de identidade. Como um JWT é apenas uma string assinada, um helper de teste que assine um token com o mesmo secret de teste é o suficiente.

import { SignJWT } from "jose";
import request from "supertest";
import app from "../app.js";

const secret = new TextEncoder().encode("test-secret");

async function tokenFor(scope = "read:posts") {
  return new SignJWT({ scope })
    .setProtectedHeader({ alg: "HS256" })
    .setSubject("user_1")
    .setIssuer("https://auth.example.com")
    .setAudience("api.example.com")
    .setExpirationTime("5m")
    .sign(secret);
}

test("rejects a request without a token", async () => {
  await request(app).get("/posts").expect(401);
});

test("accepts a request with a valid token", async () => {
  const token = await tokenFor();
  await request(app)
    .get("/posts")
    .set("authorization", `Bearer ${token}`)
    .expect(200);
});

Teste também os casos negativos: um token expirado, um token com a audience incorreta e um token assinado com uma chave diferente. Essas são as verificações que protegem sua aplicação, portanto, elas merecem cobertura tanto quanto o caminho feliz (happy path).

Melhores práticas

  • Sempre defina exp; mantenha os access tokens com duração de 15 minutos ou menos.
  • Fixe o algoritmo no verifier e rejeite alg: none.
  • Valide iss, aud, exp e nbf; nunca confie em um claim que você não verificou.
  • Mantenha secrets e chaves privadas fora do controle de versão e carregue-as de um secret manager.
  • Armazene refresh tokens com hash e realize a rotação a cada uso.
  • Mantenha os claims pequenos e não sensíveis; o payload é legível.
  • Em navegadores, prefira cookies httpOnly ou memória em vez de localStorage.
  • Planeje a revogação com tempos de vida curtos, uma deny-list de jti ou uma versão do token.
  • Use jose para códigos novos; ele é baseado em promises e portátil entre runtimes.

Erros comuns

  • Assumir que o payload está criptografado só porque parece um amontoado de caracteres aleatórios.
  • Ler claims com decode e tratá-las como verificadas.
  • Permitir que o header alg do token escolha o algoritmo de verificação.
  • Aceitar um token sem verificar o aud, fazendo com que tokens de staging funcionem em produção.
  • Usar um secret fraco ou compartilhado, ou commitá-lo no repositório.
  • Definir a expiração para 30 dias porque fazer o refresh é incômodo.
  • Armazenar tokens no localStorage e achar que o problema está resolvido.
  • Colocar uma role no token e nunca invalidá-la quando a role muda.
  • Tratar 401 e 403 como se fossem o mesmo erro.

Próximos passos

JWTs são apenas uma ferramenta dentro de um kit de identidade mais amplo. O guia de OAuth 2.0 mostra como os tokens são obtidos através de autorização delegada, Session Auth aborda a alternativa baseada em cookies para quando você precisa de revogação instantânea, e API Keys explica credenciais de longa duração para clientes de máquina. Se você quiser ver este código de verificação dentro de um servidor real, leia Node.js.

Na pratica

Emitir, verificar, rotacionar, inspecionar

As quatro operações que você escreverá primeiro, usando jose.

tokens.ts
import { SignJWT } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

export async function issueAccessToken(userId: string, scope: string) {
  return new SignJWT({ scope })
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setSubject(userId)
    .setIssuer("https://auth.example.com")
    .setAudience("api.example.com")
    .setIssuedAt()
    .setExpirationTime("15m")
    .setJti(crypto.randomUUID())
    .sign(secret);
}

HS256 vs RS256

O simétrico é mais simples quando um único serviço emite e verifica. O assimétrico vale a configuração quando a verificação se espalha por vários serviços.

HS256
import { SignJWT, jwtVerify } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

const token = await new SignJWT({ role: "editor" })
  .setProtectedHeader({ alg: "HS256" })
  .setSubject("user_42")
  .setExpirationTime("15m")
  .sign(secret);

await jwtVerify(token, secret, { algorithms: ["HS256"] });
RS256
import { importPKCS8, importSPKI, SignJWT, jwtVerify } from "jose";

const privateKey = await importPKCS8(process.env.JWT_PRIVATE_KEY!, "RS256");
const publicKey = await importSPKI(process.env.JWT_PUBLIC_KEY!, "RS256");

const token = await new SignJWT({ role: "editor" })
  .setProtectedHeader({ alg: "RS256", kid: "2026-09" })
  .setSubject("user_42")
  .setExpirationTime("15m")
  .sign(privateKey);

await jwtVerify(token, publicKey, { algorithms: ["RS256"] });

httpOnly cookie vs localStorage

O JavaScript não consegue ler um cookie httpOnly, o que remove o caminho mais comum de um bug de XSS para o sequestro total de conta.

Preferir
Set-Cookie: access_token=eyJhbGciOi...;
  HttpOnly;
  Secure;
  SameSite=Lax;
  Path=/;
  Max-Age=900
Evitar
localStorage.setItem("access_token", token);

// Any injected script can now read the token and
// exfiltrate it. Persistent XSS becomes account takeover.

Trade-offs

A autenticação stateless vale a pena?

JWTs trocam a facilidade de revogação pela facilidade de escalonamento. Decida qual delas seu produto realmente precisa.

Strengths

  • Sem store de sessão compartilhada

    Qualquer instância pode verificar um token com uma chave que já possui, mantendo simples o escalonamento horizontal e deploys multi-região.

  • Compacto e portátil

    Um único header carrega identidade, scopes e expiração entre serviços, linguagens e runtimes sem a necessidade de uma camada de tradução.

  • Ótimo para service-to-service

    Serviços independentes podem verificar tokens localmente, o que remove a dependência síncrona do servidor de autorização.

Trade-offs

  • A revogação é a parte difícil

    Um token assinado é válido até expirar. Deslogar um usuário ou revogar uma role não consegue alcançar um token que já está nas mãos de um cliente.

  • O payload é público

    Base64url não é criptografia. Qualquer coisa que você coloque nas claims é legível pelo detentor e por qualquer pessoa que o intercepte.

  • Pequenos erros são graves

    Confiar no algoritmo do header, pular a verificação de audience ou armazenar tokens no localStorage transformam a conveniência em uma brecha de segurança.

Perguntas frequentes

Perguntas frequentes

Keep learning

Related topics from the roadmap.

$ comecar a aprender

Pronto para aprender JWT (JSON Web Tokens)?

Nosso tutorial interativo te guia por JWT (JSON Web Tokens) passo a passo — com quizzes e codigo real que voce pode executar no navegador.