Prorrogação automática de expiração do JWT (JSON Web Token)

Eu gostaria de implementar a autenticação baseada em JWT para nossa nova API REST. Mas como a expiração é definida no token, é possível prolongá-lo automaticamente? Não quero que os usuários precisem fazer login após cada X minutos se estiverem usando ativamente o aplicativo nesse período. Isso seria uma enorme falha de UX.

Mas prolongar a expiração cria um novo token (e o antigo ainda é válido até expirar). E gerar um novo token após cada solicitação parece tolo para mim. Soa como um problema de segurança quando mais de um token é válido ao mesmo tempo. Claro que eu poderia invalidar o antigo usado usando uma lista negra, mas eu precisaria armazenar os tokens. E um dos benefícios do JWT é sem armazenamento.

Eu encontrei como Auth0 resolveu. Eles usam não apenas o token JWT, mas também um token de atualização: https://docs.auth0.com/refresh-token

Mas, novamente, para implementar isso (sem Auth0) eu precisaria armazenar tokens de atualização e manter sua expiração. Qual é o benefício real então? Por que não ter apenas um token (não JWT) e manter a expiração no servidor?

Existem outras opções? Está usando o JWT não adequado para este cenário?

Eu trabalho no Auth0 e estava envolvido no design do recurso de atualização de token.

Tudo depende do tipo de aplicação e aqui está a nossa abordagem recomendada.

Aplicativos da web

Um bom padrão é atualizar o token antes que ele expire.

Defina a expiração do token para uma semana e atualize o token toda vez que o usuário abrir o aplicativo da Web e a cada uma hora. Se um usuário não abrir o aplicativo por mais de uma semana, ele precisará fazer o login novamente e esse é um UX de aplicativo da web aceitável.

Para atualizar o token, sua API precisa de um novo nó de extremidade que receba um JWT válido e não expirado e retorne o mesmo JWT assinado com o novo campo de expiração. Em seguida, o aplicativo da web armazenará o token em algum lugar.

Aplicativos móveis / nativos

A maioria dos aplicativos nativos faz o login uma vez e somente uma vez.

A idéia é que o token de atualização nunca expire e pode ser trocado sempre por um JWT válido.

O problema com um token que nunca expira é que nunca significa nunca. O que você faz se perder seu telefone? Portanto, ele precisa ser identificável pelo usuário de alguma forma e o aplicativo precisa fornecer uma maneira de revogar o access. Decidimos usar o nome do dispositivo, por exemplo, “iPad de maryo”. Em seguida, o usuário pode acessar o aplicativo e revogar o access ao “iPad de maryo”.

Outra abordagem é revogar o token de atualização em events específicos. Um evento interessante está mudando a senha.

Acreditamos que o JWT não é útil para esses casos de uso, então usamos uma string gerada aleatoriamente e a armazenamos do nosso lado.

No caso em que você lida com a autenticação por conta própria (ou seja, não use um provedor como o Auth0), o seguinte pode funcionar:

  1. Emita o token JWT com expiração relativamente curta, digamos 15min.
  2. O aplicativo verifica a data de expiração do token antes de qualquer transação que exija um token (o token contém a data de expiração). Se o token expirar, ele primeiro pedirá à API para “atualizar” o token (isso é feito de maneira transparente para o UX).
  3. A API obtém a solicitação de atualização de token, mas primeiro verifica o database do usuário para ver se um sinalizador ‘reauth’ foi definido com relação a esse perfil de usuário (o token pode conter o ID do usuário). Se o sinalizador estiver presente, a atualização do token será negada, caso contrário, um novo token será emitido.
  4. Repetir.

O sinalizador ‘reauth’ no back-end do database seria definido quando, por exemplo, o usuário redefinir sua senha. O sinalizador é removido quando o usuário faz login na próxima vez.

Além disso, digamos que você tenha uma política pela qual o usuário deve fazer login pelo menos uma vez a cada 72 horas. Nesse caso, a lógica de atualização do token da API também verificaria a última data de login do usuário no database do usuário e negaria / permitiria a atualização do token nessa base.

Eu estava mexendo quando movemos nossos aplicativos para HTML5 com RESTful apis no backend. A solução que eu encontrei foi:

  1. O cliente recebe um token com um tempo de session de 30 minutos (ou qualquer que seja o tempo de session normal do servidor) após o login bem-sucedido.
  2. Um cronômetro do lado do cliente é criado para chamar um serviço para renovar o token antes do tempo de expiração. O novo token replaceá o existente em futuras chamadas.

Como você pode ver, isso reduz as solicitações frequentes de token de atualização. Se o usuário fechar o navegador / aplicativo antes que a chamada de token renovada seja acionada, o token anterior expirará a tempo e o usuário precisará fazer o login novamente.

Uma estratégia mais complicada pode ser implementada para atender a inatividade do usuário (por exemplo, negligenciar uma guia do navegador aberta). Nesse caso, a chamada de token renovada deve include o tempo de expiração esperado que não deve exceder o tempo de session definido. O aplicativo terá que acompanhar a última interação do usuário de acordo.

Eu não gosto da idéia de definir expiração longa, portanto, essa abordagem pode não funcionar bem com aplicativos nativos que exigem autenticação menos freqüente.

Uma solução alternativa para invalidar JWTs, sem nenhum armazenamento seguro adicional no backend, é implementar uma nova coluna de números inteiros jwt_version na tabela de usuários. Se o usuário desejar efetuar logout ou expirar os tokens existentes, eles simplesmente incrementarão o campo jwt_version .

Ao gerar um novo JWT, codifique o jwt_version na carga útil do JWT, opcionalmente incrementando o valor de antemão se o novo JWT deve replace todos os outros.

Ao validar o JWT, o campo jwt_version é comparado ao lado do user_id e a autorização é concedida apenas se corresponder.

Boa pergunta – e há riqueza de informações na questão em si.

O artigo Atualizar tokens: quando usá-los e como eles interagem com as JWTs fornece uma boa ideia para esse cenário. Alguns pontos são: –

  • Os tokens de atualização carregam as informações necessárias para obter um novo token de access.
  • Os tokens de atualização também podem expirar, mas são de longa duração.
  • Os tokens de atualização geralmente estão sujeitos a requisitos de armazenamento estritos para garantir que não vazem.
  • Eles também podem ser colocados na lista negra pelo servidor de autorização.

Também dê uma olhada em auth0 / angular-jwt angularjs

Para API da Web. leia Habilitar tokens de atualização do OAuth no aplicativo AngularJS usando o ASP .NET Web API 2 e Owin

Eu realmente implementei isso em PHP usando o cliente Guzzle para fazer uma biblioteca cliente para a API, mas o conceito deve funcionar para outras plataformas.

Basicamente, eu emito dois tokens, um curto (5 minutos) e um longo que expira depois de uma semana. A biblioteca cliente usa o middleware para tentar uma atualização do token curto, se receber uma resposta 401 para alguma solicitação. Em seguida, ele tentará a solicitação original novamente e, se conseguir atualizar, obterá a resposta correta, de forma transparente para o usuário. Se falhar, enviará o 401 para o usuário.

Se o token curto expirar, mas ainda assim for autêntico e o token longo for válido e autêntico, ele será atualizado com um ponto de extremidade especial no serviço que o token longo autentica (essa é a única coisa que pode ser usada). Em seguida, ele usará o token curto para obter um novo token longo, estendendo-o por mais uma semana toda vez que atualizar o token curto.

Essa abordagem também nos permite revogar o access em no máximo 5 minutos, o que é aceitável para nosso uso sem ter que armazenar uma lista negra de tokens.

Edição tardia: Relembrando este mês depois que ele ficou novo na minha cabeça, devo salientar que você pode revogar o access ao atualizar o token curto porque ele dá uma oportunidade para chamadas mais caras (por exemplo, chamar o database para ver se o usuário foi banido) sem pagar por ele em todas as chamadas para o seu serviço.

jwt-autorefresh

Se você estiver usando o nó (React / Redux / Universal JS), você pode instalar o npm i -S jwt-autorefresh .

Essa biblioteca planeja a atualização de tokens JWT em um número de segundos calculado pelo usuário antes do vencimento do token de access (com base na declaração de exp codificada no token). Ele possui um extenso conjunto de testes e verifica algumas condições para garantir que qualquer atividade estranha seja acompanhada por uma mensagem descritiva relacionada a configurações incorretas do seu ambiente.

Implementação de exemplo completa

 import autorefresh from 'jwt-autorefresh' /** Events in your app that are triggered when your user becomes authorized or deauthorized. */ import { onAuthorize, onDeauthorize } from './events' /** Your refresh token mechanism, returning a promise that resolves to the new access tokenFunction (library does not care about your method of persisting tokens) */ const refresh = () => { const init = { method: 'POST' , headers: { 'Content-Type': `application/x-www-form-urlencoded` } , body: `refresh_token=${localStorage.refresh_token}&grant_type=refresh_token` } return fetch('/oauth/token', init) .then(res => res.json()) .then(({ token_type, access_token, expires_in, refresh_token }) => { localStorage.access_token = access_token localStorage.refresh_token = refresh_token return access_token }) } /** You supply a leadSeconds number or function that generates a number of seconds that the refresh should occur prior to the access token expiring */ const leadSeconds = () => { /** Generate random additional seconds (up to 30 in this case) to append to the lead time to ensure multiple clients dont schedule simultaneous refresh */ const jitter = Math.floor(Math.random() * 30) /** Schedule autorefresh to occur 60 to 90 seconds prior to token expiration */ return 60 + jitter } let start = autorefresh({ refresh, leadSeconds }) let cancel = () => {} onAuthorize(access_token => { cancel() cancel = start(access_token) }) onDeauthorize(() => cancel()) 

disclaimer: Eu sou o mantenedor

Como sobre esta abordagem:

  • Para cada solicitação do cliente, o servidor compara o expirationTime do token com (currentTime – lastAccessTime)
  • Se expirationTime < (currentTime - lastAccessedTime) , ele alterará o último lastAccessedTime para currentTime.
  • Em caso de inatividade no navegador por um período de tempo superior a expirationTime ou caso a janela do navegador tenha sido encerrada e o expirationTime> (currentTime – lastAccessedTime) , o servidor possa expirar o token e solicitar que o usuário faça o login novamente.

Nós não precisamos de um end point adicional para atualizar o token neste caso. Gostaria de receber qualquer feedack.

Eu resolvi esse problema adicionando uma variável nos dados do token:

 softexp - I set this to 5 mins (300 seconds) 

Eu defino expiresIn opção para o meu tempo desejado antes que o usuário seja forçado a fazer o login novamente. O meu está definido para 30 minutos. Isso deve ser maior que o valor de softexp .

Quando meu aplicativo do lado do cliente envia uma solicitação à API do servidor (onde o token é necessário, por exemplo, página de lista de clientes), o servidor verifica se o token enviado ainda é válido ou não com base no valor de expiração original ( expiresIn ). Se não for válido, o servidor responderá com um status específico para esse erro, por exemplo. INVALID_TOKEN .

Se o token ainda é válido com base no valor expiredIn , mas já excedeu o valor do softexp , o servidor responderá com um status separado para este erro, por exemplo. EXPIRED_TOKEN :

 (Math.floor(Date.now() / 1000) > decoded.softexp) 

No lado do cliente, se recebeu a resposta EXPIRED_TOKEN , deverá renovar o token automaticamente enviando uma solicitação de renovação ao servidor. Isso é transparente para o usuário e está sendo cuidado automaticamente do aplicativo cliente.

O método de renovação no servidor deve verificar se o token ainda é válido:

 jwt.verify(token, secret, (err, decoded) => {}) 

O servidor recusará a renovação de tokens se falhar o método acima.