Token recusado com "jwt expired" logo depois do login? Confira o relógio
Descubra quando o token é recusado por diferença de relógio entre servidores, meça o desvio, sincronize com NTP e configure uma folga de segundos, não de horas.
O login funciona no notebook, funciona em homologação e, em produção, uma parte das pessoas cai de volta na tela de entrada segundos depois de entrar. O log da API diz jwt expired para um token emitido há 20 segundos. O suporte manda limpar cookies. Ninguém olha o relógio da VM, que estava seis minutos adiantado.
A resposta curta: quando um token novinho é recusado como expirado ou "ainda não válido", compare o relógio de quem emite com o de quem valida. Sincronize todos os servidores com NTP, configure uma folga pequena (de segundos até um ou dois minutos) na validação e registre a diferença sempre que recusar por tempo. Não aumente a folga para horas e muito menos desligue a checagem de expiração.
Por que um relógio errado parece bug de login
Um JWT carrega horários absolutos, em segundos desde 1970: iat (quando foi emitido), nbf (a partir de quando vale) e exp (até quando vale). Quem emite preenche esses campos com o próprio relógio. Quem valida compara com o seu relógio. Se os dois discordam, a conta sai errada mesmo com código perfeito.
Veja o caso concreto. O servidor de identidade emite um token de acesso com validade de 5 minutos às 10:00:00 pelo relógio dele. A API que valida está 6 minutos adiantada: para ela, são 10:06:00 quando o token chega. O exp diz 10:05:00. Resultado: expirado na chegada, para todo mundo que bate naquela instância.
O inverso também quebra. Se a API está atrasada e o emissor preenche nbf, o token parece vir do futuro e é recusado como "ainda não ativo". Em bibliotecas Node.js como jsonwebtoken, isso aparece como NotBeforeError com a mensagem jwt not active.
O que torna o problema traiçoeiro é que ele é parcial. Só uma das instâncias está com o relógio torto, então metade dos pedidos passa e metade falha, e o balanceador faz parecer aleatório.
Onde o relógio costuma desviar
- VM restaurada de snapshot ou pausada: volta com o horário do momento da pausa até o serviço de tempo corrigir.
- Servidor sem serviço de NTP ativo: o relógio de hardware deriva alguns segundos por dia e, em meses, vira minutos.
- Firewall bloqueando a porta 123/UDP: o serviço de tempo está instalado, mas não consegue falar com ninguém.
- Máquina de desenvolvimento com hora ajustada à mão: para testar uma regra de data, alguém mudou o relógio e esqueceu.
Containers usam o relógio do host, então não adianta procurar NTP dentro do container. Conserte no host ou na VM.
Como diagnosticar e corrigir
- Meça o desvio em cada máquina envolvida. Emissor, API, workers e o host dos containers.
- Ligue a sincronização onde estiver desligada e confirme que ela está realmente sincronizada, não só instalada.
- Configure uma folga pequena na validação, explícita no código, para absorver os poucos segundos que sobram mesmo com NTP.
- Registre a diferença quando recusar por tempo, para que a próxima ocorrência se diagnostique sozinha.
- Monitore o desvio e alerte quando passar de alguns segundos.
Medindo no Linux e no Windows
Em distribuições com systemd, timedatectl diz se o relógio está sincronizado. Se a máquina usa chrony, chronyc tracking mostra o desvio em relação à fonte de tempo:
$ timedatectl
Local time: sex 2026-10-16 10:06:02 -03
Universal time: sex 2026-10-16 13:06:02 UTC
System clock synchronized: no
NTP service: inactive
$ sudo timedatectl set-ntp true
$ chronyc tracking | grep "System time"
System time : 0.000041 seconds slow of NTP time
No Windows, w32tm /query /status mostra a fonte e a última sincronização, e w32tm /resync força uma nova. Num servidor em domínio Active Directory, a hierarquia de tempo vem do controlador de domínio, então o problema pode estar lá.
Repare na linha System clock synchronized: no do exemplo: é exatamente a VM da história, com o serviço inativo. Depois de ligar o NTP, o desvio caiu para microssegundos.
Folga explícita na validação
Mesmo sincronizados, relógios divergem por frações de segundo, e a rede adiciona latência. Por isso as bibliotecas aceitam uma tolerância. No jsonwebtoken para Node.js, a opção se chama clockTolerance e é em segundos:
const jwt = require('jsonwebtoken');
function validarToken(token, chavePublica) {
try {
return jwt.verify(token, chavePublica, {
algorithms: ['RS256'],
issuer: 'https://login.loja.exemplo',
audience: 'api-pedidos',
clockTolerance: 30
});
} catch (err) {
if (err.name === 'TokenExpiredError' || err.name === 'NotBeforeError') {
const dados = jwt.decode(token) || {};
const agora = Math.floor(Date.now() / 1000);
logger.warn({
motivo: err.name,
iat: dados.iat,
agoraLocal: agora,
desvioDesdeEmissao: dados.iat ? agora - dados.iat : null
}, 'token recusado por tempo');
}
throw err;
}
}
O campo desvioDesdeEmissao é o que salva a investigação. Um valor negativo significa que o token foi emitido "no futuro" segundo o relógio local, ou seja, o emissor está adiantado ou a API está atrasada. Um valor maior que a validade do token, num token que acabou de ser emitido, aponta o problema contrário. Note que o log registra horários, não o token, seguindo o que o post de log sem senha recomenda.
No ASP.NET Core, a propriedade equivalente é ClockSkew em TokenValidationParameters, e o padrão é de 5 minutos. Vale saber disso por dois motivos: explica por que uma API .NET "aguenta" um relógio torto que derruba uma API Node ao lado, e mostra que um token de 5 minutos pode, na prática, valer até 10. Se você quer prazos curtos de verdade, reduza a folga de forma consciente.
Quanto de folga é razoável
Pense na folga como parte da validade do token. Um token de acesso de 5 minutos com 30 segundos de tolerância vale, no pior caso, 5 minutos e meio, o que ainda é curto. Com uma hora de tolerância, a expiração deixa de existir na prática e um token vazado continua útil muito tempo depois, contrariando tudo o que o guia de JWT que expira constrói.
Se você precisa de mais do que um ou dois minutos de folga para as coisas funcionarem, o problema é a sincronização, não a configuração do token.
Erros comuns
- Desligar a checagem de expiração para "resolver": o erro some e qualquer token roubado passa a valer para sempre.
- Aumentar a folga para horas: o mesmo efeito, só que disfarçado de configuração.
- Procurar NTP dentro do container: o relógio é do host, e a correção feita no lugar errado não muda nada.
- Logar só "token inválido": sem
iate horário local, ninguém associa o erro ao relógio e a investigação vai para o código. - Confundir fuso horário com relógio errado: os campos do JWT são em UTC; ajustar fuso não corrige um relógio adiantado, tema diferente do tratado em fuso horário no PHP.
- Testar só numa instância: o problema aparece apenas na máquina com relógio torto, e o teste passa na outra.
Checklist
- Todos os servidores que emitem ou validam token mostram relógio sincronizado.
- A porta 123/UDP de saída está liberada para as fontes de tempo usadas.
- A tolerância de relógio está explícita no código e é de segundos ou poucos minutos.
- Recusas por tempo geram log com
iat, horário local e diferença calculada. - Existe alerta quando o desvio de alguma máquina passa de alguns segundos.
- VMs restauradas de snapshot passam por conferência de horário antes de voltar ao balanceador.
Rode o comando de status de tempo no servidor que emite o token e no que valida: a quantos segundos um relógio está do outro neste momento?
Referência: https://www.ntp.org/documentation/