August 27, 2026
JWT não é autenticação: entendendo Access Token, Refresh Token e Rotação
Conhecido como JSON Web Token (JWT), ele está presente em muitas aplicações web modernas. Quando começamos a estudar autenticação, é comum…
By Matheus Iostrc
6 min read
Conhecido como JSON Web Token (JWT), ele está presente em muitas aplicações web modernas. Quando começamos a estudar autenticação, é comum encontrar frases como "vamos utilizar JWT para fazer a autenticação".
O JWT é um formato compacto para representar e transmitir informações, chamadas de claims, entre diferentes partes de uma aplicação. Essas informações podem ser assinadas digitalmente, permitindo que o servidor verifique se o token foi alterado depois de ser emitido.
Um JWT possui três partes principais:
- Header
- Payload
- Signature
Como podemos observar na imagem, essas três partes aparecem separadas por pontos quando o token está codificado:
Header
O Header contém informações sobre o próprio token, como o algoritmo utilizado para gerar sua assinatura e o tipo do token.
{
"alg": "HS256",
"typ": "JWT"
}{
"alg": "HS256",
"typ": "JWT"
}Payload
O Payload contém os dados, ou claims, que queremos transportar dentro do token.
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}É importante entender que o Payload não é criptografado por padrão. Portanto, não devemos colocar informações sensíveis, como senhas, dentro de um JWT.
Signature
A Signature é utilizada para verificar se o token foi alterado depois de sua criação.
secret-keysecret-keyO resultado final é um token semelhante a:
xxxxx.yyyyy.zzzzzxxxxx.yyyyy.zzzzz
A autenticação envolve todo o processo de verificar quem é o usuário e controlar o acesso dele à aplicação. O JWT pode fazer parte desse processo, mas não resolve tudo sozinho.js
Então JWT é autenticação?
Não.
Essa é provavelmente a principal confusão quando começamos a trabalhar com JWT.
A autenticação envolve todo o processo de verificar quem é o usuário. Já a autorização determina o que esse usuário pode acessar ou executar.
O JWT pode participar desse processo, mas ele não resolve sozinho questões como:
- Como o usuário realiza login?
- Onde as credenciais são verificadas?
- Onde o token será armazenado?
- Quando o token deve expirar?
- Como renovar uma sessão?
- Como invalidar uma sessão?
- O que acontece se um token for roubado?
É nesse ponto que entram conceitos como Access Token, Refresh Token e Refresh Token Rotation.
E entender essa diferença é importante porque utilizar JWT em uma aplicação não significa automaticamente que a autenticação está segura.
Access Token
O Access Token é o token que o cliente apresenta ao backend para provar que possui uma sessão autenticada e acessar recursos protegidos.
Por exemplo, imagine que o usuário faça login:
POST /auth/login
email: matheus@email.com
senha: ********POST /auth/login
email: matheus@email.com
senha: ********O backend valida as credenciais e pode retornar:
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}Nas próximas requisições, o Access Token é enviado:
GET /pets
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Authorization: Bearer eyJhbGciOiJIUzI1NiIs..GET /pets
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Authorization: Bearer eyJhbGciOiJIUzI1NiIs..O backend então verifica o token e, se estiver válido, permite o acesso ao recurso.
A principal característica do Access Token é que ele deve ter uma vida relativamente curta. Por exemplo:
Access Token
├── Criado: 10:00
├── Expiração: 11:00
└── Validade: 1 horaAccess Token
├── Criado: 10:00
├── Expiração: 11:00
└── Validade: 1 horaMas por que tão pouco tempo?
Porque, caso o token seja roubado, existe uma janela limitada em que ele pode ser utilizado.
Isso nos leva a um problema.
Se o Access Token expira em uma hora, o usuário precisa fazer login novamente a cada hora?
Não.
É justamente para isso que existe o Refresh Token.
Refresh Token
O Refresh Token é utilizado para renovar a sessão do usuário sem exigir que ele informe novamente suas credenciais.
Quando o Access Token expira:
Access Token
│
▼
EXPIRADO
│
│ Refresh Token
▼
Backend
│
▼
Novo Access TokenAccess Token
│
▼
EXPIRADO
│
│ Refresh Token
▼
Backend
│
▼
Novo Access TokenO cliente envia o Refresh Token para um endpoint específico:
POST /auth/refreshPOST /auth/refreshO backend verifica se o Refresh Token é válido.
Se estiver tudo correto, ele pode emitir um novo Access Token:
{
"accessToken": "novo-access-token"
}{
"accessToken": "novo-access-token"
}O usuário continua navegando normalmente, sem perceber que houve uma renovação da sessão.
Mas por que não deixar o Access Token durar 30 dias?
Essa é uma das perguntas mais importantes para entender essa arquitetura.
Imagine que você tenha apenas um token:
Access Token
└── Expira em 30 diasAccess Token
└── Expira em 30 diasSe esse token for roubado, o atacante potencialmente poderá utilizá-lo durante todo esse período.
Agora imagine:
Access Token
└── Expira em 15 minutos
Refresh Token
└── Vida mais longaAccess Token
└── Expira em 15 minutos
Refresh Token
└── Vida mais longaO Access Token possui uma janela de utilização muito menor.
O Refresh Token fica responsável por manter a sessão.
Assim conseguimos separar duas responsabilidades:
Access Token → acessar a API
Refresh Token → renovar a sessão
E onde entra a rotação?
Aqui começa uma parte ainda mais interessante.
Um erro comum seria manter o mesmo Refresh Token durante toda a sessão:
Refresh Token A
│
├── gera Access Token
│
├── gera Access Token
│
├── gera Access Token
│
└── continua válido...Refresh Token A
│
├── gera Access Token
│
├── gera Access Token
│
├── gera Access Token
│
└── continua válido...Se esse Refresh Token for roubado, ele pode se tornar um problema.
Com Refresh Token Rotation, cada utilização do Refresh Token gera um novo Refresh Token e invalida o anterior:
Refresh Token A
│
▼
/refresh
│
├── Access Token B
│
└── Refresh Token B
│
▼
Token A inválidoRefresh Token A
│
▼
/refresh
│
├── Access Token B
│
└── Refresh Token B
│
▼
Token A inválidoNa próxima renovação:
Refresh Token B
│
▼
/refresh
│
├── Access Token C
│
└── Refresh Token C
Token B → inválidoRefresh Token B
│
▼
/refresh
│
├── Access Token C
│
└── Refresh Token C
Token B → inválidoOu seja, a sessão vai "girando" os Refresh Tokens.
O fluxo completo
Agora podemos juntar todos esses conceitos em um único fluxo.
Quando o usuário realiza login, o backend valida suas credenciais e cria os tokens:
Usuário
│
│ email + senha
▼
Backend
│
├── Access Token
│
└── Refresh TokenUsuário
│
│ email + senha
▼
Backend
│
├── Access Token
│
└── Refresh TokenO Access Token é utilizado para acessar os recursos protegidos da aplicação:
Frontend
│
│ Authorization: Bearer <access_token>
▼
Backend
│
├── Token válido → continua
│
└── Token inválido/expirado → 401Frontend
│
│ Authorization: Bearer <access_token>
▼
Backend
│
├── Token válido → continua
│
└── Token inválido/expirado → 401Quando o Access Token expira, o frontend pode utilizar o Refresh Token para solicitar uma nova sessão:
Frontend
│
│ Refresh Token
▼
POST /auth/refresh
│
▼
Backend
│
├── Novo Access Token
└── Novo Refresh TokenFrontend
│
│ Refresh Token
▼
POST /auth/refresh
│
▼
Backend
│
├── Novo Access Token
└── Novo Refresh TokenDessa maneira, o usuário consegue permanecer autenticado sem precisar realizar login novamente toda vez que o Access Token expirar.
E onde armazenar esses tokens?
Aqui começamos a entrar em uma discussão importante de segurança.
Não basta simplesmente criar os tokens. Também precisamos pensar em onde eles serão armazenados.
Uma possibilidade seria utilizar localStorage:
localStorage.setItem("accessToken", token);localStorage.setItem("accessToken", token);É simples, mas existe uma preocupação importante: JavaScript consegue acessar esse valor.
Em uma aplicação vulnerável a XSS, um script malicioso poderia potencialmente acessar tokens armazenados dessa maneira.
Por isso, uma estratégia bastante utilizada é armazenar tokens de sessão em cookies HttpOnly.
Set-Cookie: refreshToken=...; HttpOnly; Secure; SameSite=LaxSet-Cookie: refreshToken=...; HttpOnly; Secure; SameSite=LaxCom HttpOnly, o cookie não pode ser acessado diretamente através de JavaScript.
Isso não significa que cookies resolvem todos os problemas de segurança, mas reduz uma superfície de ataque importante.
Além disso, dependendo da arquitetura, podemos utilizar:
Secure, para enviar o cookie somente através de HTTPS;SameSite, para ajudar na proteção contra ataques CSRF;HttpOnly, para impedir acesso ao cookie através de JavaScript.
A configuração correta depende do ambiente e da arquitetura da aplicação.
JWT é criptografado?
Essa é outra confusão bastante comum.
Por padrão, JWT não significa criptografia.
Um JWT assinado pode ser decodificado e seu Payload pode ser lido.
Por exemplo:
{
"sub": "123",
"email": "user@example.com",
"role": "admin"
}{
"sub": "123",
"email": "user@example.com",
"role": "admin"
}A assinatura serve para verificar a integridade e autenticidade do token, não para esconder seu conteúdo.
Por isso, informações extremamente sensíveis não devem ser colocadas no Payload esperando que estejam protegidas.
Uma forma simples de pensar é:
JWT assinado
│
├── Posso verificar se foi alterado?
│ SIM
│
└── O conteúdo está escondido?
NÃOJWT assinado
│
├── Posso verificar se foi alterado?
│ SIM
│
└── O conteúdo está escondido?
NÃOE se o Refresh Token for roubado?
Aqui chegamos a uma das razões pelas quais a Refresh Token Rotation é interessante.
Imagine que o usuário tenha:
Refresh Token ARefresh Token AEle utiliza esse token para renovar sua sessão:
Refresh Token A
│
▼
/refresh
│
├── Access Token B
└── Refresh Token BRefresh Token A
│
▼
/refresh
│
├── Access Token B
└── Refresh Token BDepois dessa operação, o Token A deve ser considerado inválido.
Agora imagine que alguém tente utilizar o Token A novamente.
Refresh Token A
│
▼
/refresh
│
X
│
└── Token já utilizadoRefresh Token A
│
▼
/refresh
│
X
│
└── Token já utilizadoEsse comportamento pode indicar que o token foi roubado ou que existe algum problema com a sessão.
O backend pode então tomar medidas como revogar a sessão ou toda a família de tokens relacionada a ela, dependendo da estratégia implementada.
Isso é conhecido como Refresh Token Reuse Detection.
Logout também é importante
Outra pergunta interessante é:
Se JWT é stateless, como fazemos logout?
Se um Access Token ainda estiver válido, simplesmente remover algo do frontend não faz com que aquele JWT deixe de existir.
Por exemplo:
Access Token
│
└── válido até 15:30Access Token
│
└── válido até 15:30Se o usuário clicar em logout às 15:00, o token continua matematicamente válido até sua expiração, caso o backend não mantenha algum mecanismo adicional de revogação.
Por isso, uma estratégia comum é manter o Access Token com uma vida curta e controlar a sessão através do Refresh Token.
No logout:
Logout
│
▼
Backend
│
├── Revoga Refresh Token
└── Remove cookie de sessãoLogout
│
▼
Backend
│
├── Revoga Refresh Token
└── Remove cookie de sessãoAssim, mesmo que o Access Token ainda não tenha expirado, ele terá uma vida útil curta e o usuário não poderá renovar a sessão utilizando o Refresh Token revogado.
Stateless não significa que não existe estado
Esse é outro ponto que considero importante.
É comum ouvir:
"JWT é stateless."
Isso pode ser verdade em relação à validação do Access Token, já que o servidor consegue verificar sua assinatura e seus claims sem necessariamente consultar uma sessão no banco.
Mas isso não significa que toda a autenticação precisa ser completamente stateless.
Podemos, por exemplo, armazenar informações relacionadas aos Refresh Tokens:
Database
Session
├── userId
├── refreshTokenHash
├── expiresAt
├── revokedAt
└── createdAtDatabase
Session
├── userId
├── refreshTokenHash
├── expiresAt
├── revokedAt
└── createdAtDessa forma, conseguimos controlar sessões, revogar tokens e detectar reutilização de Refresh Tokens.
Ou seja, podemos ter uma arquitetura híbrida:
Access Token
│
└── validação rápida
Refresh Token
│
└── controle de sessão
│
└── DatabaseAccess Token
│
└── validação rápida
Refresh Token
│
└── controle de sessão
│
└── Database
Essa separação pode oferecer um bom equilíbrio entre performance, segurança e controle.
JWT não é uma solução mágica
Depois de entender todo esse fluxo, fica mais fácil perceber por que simplesmente instalar uma biblioteca JWT não significa que a autenticação da aplicação está pronta.
Ainda precisamos tomar decisões sobre:
- Expiração dos tokens;
- Armazenamento;
- Rotação;
- Revogação;
- Logout;
- XSS;
- CSRF;
- Reutilização de Refresh Tokens;
- Gerenciamento de sessões;