Programação Por Equipe DevBras

Capa do artigo: Token recusado com "jwt expired" logo depois do login? Confira o relógio

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

  1. Meça o desvio em cada máquina envolvida. Emissor, API, workers e o host dos containers.
  2. Ligue a sincronização onde estiver desligada e confirme que ela está realmente sincronizada, não só instalada.
  3. Configure uma folga pequena na validação, explícita no código, para absorver os poucos segundos que sobram mesmo com NTP.
  4. Registre a diferença quando recusar por tempo, para que a próxima ocorrência se diagnostique sozinha.
  5. 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 iat e 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?

Avalie este artigo

Referência: https://www.ntp.org/documentation/