Pular para conteúdo

0034 — Refresh do JWT por token e teto absoluto de sessão

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-15 · Decidido em: 2026-09-15

Contexto

Teste de papel e inativação no bi-comercial com usuário pAbast, em 2026-09-15: o usuário inativado seguiu com acesso, e a troca de papel (LUBRIFICANTE → COMERCIAL) não apareceu nem com F5. As duas coisas só refletiam com logout/login.

A causa foi medida no código. A sessão pAbast guarda a senha só em memória, e o app renovava o JWT re-logando em /api/auth/pabast/login. Depois de um F5 a senha some e não há como renovar: o papel fica congelado até o JWT vencer (AUTH_TOKEN_EXPIRATION_MINUTES, 24 h). O central não tem revogação no servidor, e o PowerSync aceita o token até o exp.

A casa fixou o desenho na ADR central 0037 e no bullet de sessão da norma de Segurança. O prazo de revogação é o TTL do access token, de no máximo 60 min. O app renova trocando o token vigente, e a sessão tem teto absoluto de no máximo 7 dias. O contrato (rota, códigos, claims) é do central, e esta decisão o registra.

Decisão

  1. POST /api/auth/refresh troca um JWT de app-user ainda válido por um novo, com a mesma decisão do login (cliente ativo, método de auth, acesso e papel atuais) e sem re-provar a credencial. Só o Authorization: Bearer, sem corpo; cliente e app saem do próprio token. Token sem oauth_app_id (legado scope=full, anônimo, xadm_admin) não tem via de refresh: 403 not_app_user.
  2. Claims novos nos tokens de app-user: sub_type (pabast|oauth) diz quem é o sub, e auth_time (epoch s) guarda o último login com credencial. O refresh copia o auth_time.
  3. Token de antes do deploy, sem sub_type, é classificado pelo email. Medido no código: o login pAbast emite sem email e os dois caminhos oauth sempre o emitem (vazio, se o Firebase não o deu). Sem auth_time, a sessão conta do iat.
  4. Teto absoluto de sessão: AUTH_SESSION_MAX_HOURS, default 168 h (7 dias). É também o máximo da norma, e valor acima é cortado, como o TTL já é cortado em 1440 min. O teto conta do auth_time. Além dele, o refresh recusa com 403 session_expired, e o exp emitido nunca passa do teto: exp = min(agora + TTL, auth_time + teto). Sem o corte, um refresh na hora 167 daria um token válido até a hora 191.
  5. pAbast: o sub precisa existir no pabast_senhas do cliente, comparado com TRIM (o ERP entrega o id com espaço na borda); senão, 403 access_revoked. Sem acesso ATIVO, o token sai sem role, em 200, como no login — refresh e login nunca discordam, e reativar reflete sem logout.
  6. oauth: só ATIVO renova; PENDENTE, REJEITADO, INATIVO ou sem linha dão 403 access_revoked. A conta @xadm segue ADMIN implícito, sem linha de acesso.
  7. Cliente apagado revoga como o desativado: 403 cliente_inactive. O login devolve 400 cliente_not_found, que o app não trata como revogação e o manteria logado até o exp.
  8. Os code de 403 são contrato com o app, que decide deslogar por eles (_codigosRevogacao no AppConnector do bi-comercial). O contrato vive no contrato da API, não na norma; código novo de revogação entra lá e no app.

Consequências

  • O refresh não revoga o token anterior: o PowerSync e toda API o aceitam até o exp. O prazo de revogação no servidor é o TTL, que a norma limita a 60 min.
  • Pendente, na ordem da ADR central 0037: o AUTH_TOKEN_EXPIRATION_MINUTES de produção segue em 1440 até os apps que renovam sessão adotarem o refresh. Baixá-lo antes faria cada app sem refresh re-logar a cada TTL. No mesmo passo, o clamp de 1440 do CentralSettings cai para 60.
  • Como o refresh não re-prova a senha, senha trocada no ERP não derruba a sessão antes do teto. É o preço do refresh, e o teto é o limite dele.
  • O rate limit é o dos logins (60 req/min por IP). Com uma chamada a cada 5 min por sessão, cabem cerca de 300 sessões atrás do mesmo IP de saída antes do 429, sem contar os eventos de foco. Com o TTL curto, o app também renova antes do exp.
  • O expires_in pode vir menor que o TTL perto do teto, e o app precisa usar o valor devolvido.
  • Tokens emitidos antes do deploy continuam renováveis (classificação pelo email, sessão pelo iat) e somem sozinhos em até 24 h.

Alternativas consideradas

  • Classificar o token legado por consulta ao app_user_access (sugestão do handoff): descartada. A conta @xadm não tem linha de acesso, cairia como pAbast e tomaria access_revoked.
  • Refresh token opaco guardado no banco: descartado na ADR central 0037. Custa tabela, rotação e detecção de reuso, e o refresh token no browser é tão exfiltrável quanto o access.
  • Teto só na recusa, sem cortar o exp: descartada, porque o último token viveria até um TTL além do teto.
  • 403 para pAbast sem papel: descartada. O refresh discordaria do login, e reativar o acesso exigiria novo login.