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

509

Gostaria de implementar a autenticação baseada em JWT em nossa nova API REST. Mas como a expiração é definida no token, é possível prolongá-lo automaticamente? Eu não quero que os usuários precisem entrar após cada X minutos se estiverem usando o aplicativo ativamente nesse período. Isso seria uma grande falha no 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 bobagem para mim. Parece 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 precisaria armazenar os tokens. E um dos benefícios do JWT é o armazenamento.

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

Mais uma vez, para implementar isso (sem Auth0), eu precisaria armazenar tokens de atualização e manter a validade deles. 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? O uso do JWT não é adequado para esse cenário?

maryo
fonte
1
Na verdade, provavelmente não há problema de segurança com muitos tokens válidos de uma só vez ... Na verdade, existe um número infinito de tokens válidos ... Então, por que ter um token de atualização? Vou regenerá-los após cada solicitação, na verdade não deve ser um problema.
Mario
1
Para SPA, verifique
wong2
2
@maryo Acho que ter (potencialmente) centenas ou milhares de JWTs válidos não utilizados no mercado a qualquer momento aumenta a sua pegada de ataque e é um risco de segurança. Na minha opinião, os JWTs devem ser emitidos com cuidado, pois são de tokens de acesso com as chaves do castelo.
Java-addict301

Respostas:

590

Trabalho na Auth0 e estive envolvido no design do recurso de token de atualização.

Tudo depende do tipo de aplicativo 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 como uma semana e atualize o token sempre que o usuário abrir o aplicativo Web e a cada uma hora. Se um usuário não abrir o aplicativo por mais de uma semana, ele precisará fazer login novamente, e esse é o UX do aplicativo Web aceitável.

Para atualizar o token, sua API precisa de um novo terminal que receba um JWT válido, 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 login uma vez e apenas uma vez.

A ideia é que o token de atualização nunca expire e possa 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 o telefone? Portanto, ele precisa ser identificado pelo usuário de alguma forma e o aplicativo precisa fornecer uma maneira de revogar o acesso. Decidimos usar o nome do dispositivo, por exemplo, "iPad da maryo". Em seguida, o usuário pode acessar o aplicativo e revogar o acesso ao "iPad da maryo".

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

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

José F. Romaniello
fonte
42
Para a abordagem recomendada para aplicativos da Web, se o token é válido por uma semana, não estamos preocupados com alguém que intercepta o token e, em seguida, é capaz de usá-lo por tanto tempo? Isenção de responsabilidade: não sei bem do que estou falando.
precisa saber é o seguinte
30
@wbeange yes interceptação é um problema, mesmo com cookies. Você deve usar https.
José F. Romaniello 27/01
15
@ JoséF.Romaniello No seu exemplo de aplicativo da Web, tudo faz sentido para mim, exceto ter que armazenar o token. Eu pensei que a beleza do JWT era a autenticação sem estado - o que significa que o aplicativo da Web NÃO precisa armazenar o token conforme ele é assinado. Eu acho que o servidor pode apenas verificar a validade do token, garantir que esteja dentro do prazo de validade e emitir um token JWT renovado. Você poderia, por favor, elaborar isso? Talvez eu ainda não entenda JWTs o suficiente.
Lo-Tan
7
Duas perguntas / preocupações: 1- Caso de aplicativo da Web: por que o token expirado não pode ser atualizado? Digamos que definimos uma expiração curta (1 hora) e faça novas chamadas para o servidor back-end quando um token expirar, como você disse. 2- Existe uma preocupação de segurança em armazenar a senha com hash (com sal aleatório) no token? A idéia é que, se estiver presente, o servidor back-end poderá verificar a senha armazenada no banco de dados quando for solicitada uma renovação e negar a solicitação se as senhas não corresponderem. Isso cobriria a alteração de senha do aplicativo Mobile / Native, permitindo que a solução fosse estendida ao caso de uso Mobile.
Psamaan
8
-1 Expor uma API pública que assina cegamente qualquer token para estender seu período de validação é ruim. Agora todos os seus tokens têm uma validade efetiva infinita. O ato de assinar um token deve incluir as verificações de autenticação apropriadas para cada reivindicação feita nesse token no momento da assinatura.
Phil
69

No caso em que você lida com a autenticação (por exemplo, não use um provedor como Auth0), o seguinte pode funcionar:

  1. Emita o token JWT com validade relativamente curta, digamos 15min.
  2. O aplicativo verifica a data de validade do token antes de qualquer transação que exija um token (o token contém a data de validade). Se o token expirou, primeiro ele solicita à API que 'atualize' o token (isso é feito de forma transparente no UX).
  3. A API recebe a solicitação de atualização de token, mas primeiro verifica o banco de dados do usuário para ver se um sinalizador 'reauth' foi definido nesse 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 banco de dados seria definido quando, por exemplo, o usuário redefinisse sua senha. O sinalizador é removido quando o usuário efetuar login na próxima vez.

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

IanB
fonte
7
Eu não acho que isso seria seguro. Se eu fosse um invasor e roubasse seu token e o enviasse ao servidor, o servidor verificaria e verificaria se o sinalizador está definido como verdadeiro, o que é ótimo, pois bloqueia uma atualização. Acho que o problema seria que, se a vítima mudasse sua senha, o sinalizador seria definido como falso e agora o invasor poderá usar esse token original para atualizar.
precisa saber é o seguinte
6
@ user2924127 nenhuma solução de autenticação é perfeita e sempre haverá trocas. Se um invasor estiver em posição de 'roubar seu token', você poderá ter problemas maiores com que se preocupar. Definir uma vida útil máxima do token seria um ajuste útil para o acima.
BNAI
27
em vez de ter outro campo no banco de dados, o sinalizador reauth, você pode incluir o hash (bcrypt_password_hash) no token. Então, ao atualizar o token, você apenas confirma se o hash (bcrypt_password_hash) é igual a um valor do token. Para negar a atualização do token, é necessário apenas atualizar o hash da senha.
bas
4
@bas, pensando em otimizações e desempenho, acho que a validação do hash da senha seria redundante e teria mais implicações no servidor. Aumente o tamanho do token para que a firma / validação da assinatura leve mais tempo. cálculos adicionais de hash para o servidor para a senha. com a abordagem de campo extra, você apenas valida no recálculo com um booleano simples. As atualizações de banco de dados são menos frequentes para o campo extra, mas as atualizações de token são mais frequentes. E você obtém o serviço opcional de força de logins individuais para qualquer sessão existente (móvel, web, etc.).
Le0diaz
6
Acho que o primeiro comentário do usuário2924127 está realmente errado. Quando a senha é alterada, a conta é marcada como requerendo uma nova autenticação, portanto, todos os tokens expirados existentes serão inválidos.
22416 Ralph
15

Eu estava mexendo ao mover nossos aplicativos para HTML5 com APIs RESTful no back-end. A solução que surgiu foi:

  1. O cliente recebe um token com um tempo de sessão de 30 minutos (ou qualquer que seja o tempo habitual da sessão no 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 expirar. O novo token substituirá o existente em chamadas futuras.

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 renovação do token seja acionada, o token anterior expirará a tempo e o usuário precisará efetuar novamente o login.

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

Não gosto da ideia de definir uma expiração longa, portanto, essa abordagem pode não funcionar bem com aplicativos nativos que exigem autenticação menos frequente.

coolersport
fonte
1
E se o computador estiver suspenso / em suspensão. O cronômetro ainda contará até a expiração, mas o token já estava realmente expirado. Temporizador não funciona nessas situações
Alex Parij
@AlexParij Você poderia comparar com um tempo fixo, algo como isto: stackoverflow.com/a/35182296/1038456
Aparajita
2
Permitir que o cliente solicite um novo token com uma data de validade preferida cheira a um risco de segurança para mim.
Java-addict301
14

Uma solução alternativa para invalidar JWTs, sem nenhum armazenamento seguro adicional no back-end, é implementar uma nova jwt_versioncoluna inteira na tabela de usuários. Se o usuário desejar fazer logout ou expirar os tokens existentes, basta aumentar o jwt_versioncampo.

Ao gerar um novo JWT, codifique-o jwt_versionna carga útil do JWT, incrementando opcionalmente o valor antecipadamente se o novo JWT deve substituir todos os outros.

Ao validar o JWT, o jwt_versioncampo é comparado ao lado de user_ide a autorização é concedida somente se corresponder.

Ollie Bennett
fonte
1
Isso tem problemas com vários dispositivos. Essencialmente, se você fizer logoff em um dispositivo, ele fará logout em qualquer lugar. Direita?
Sam Washburn
4
Ei, isso pode não ser um "problema", dependendo de seus requisitos, mas você está certo; isso não suporta o gerenciamento de sessões por dispositivo.
Ollie Bennett
Isso não significa que o jwt_version precisa ser armazenado no servidor, de modo que o esquema de autenticação se torne "semelhante a uma sessão" e derrote o objetivo fundamental dos JWTs?
ChetPrickles
8

Boa pergunta - e há muitas informações na própria pergunta.

O artigo Atualizar tokens: quando usá-los e como eles interagem com os 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 acesso.
  • Os tokens de atualização também podem expirar, mas duram bastante.
  • Os tokens de atualização geralmente estão sujeitos a requisitos rígidos de armazenamento para garantir que não vazem.
  • Eles também podem ser colocados na lista negra pelo servidor de autorização.

Veja também 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

LCJ
fonte
Talvez eu tenha lido errado ... Mas o artigo com um título que começa como "Atualizar tokens ..." não contém nada sobre tokens de atualização, exceto o que você mencionou aqui.
Ievgen Martynov
8

Na verdade, eu implementei isso em PHP usando o cliente Guzzle para criar uma biblioteca cliente para a API, mas o conceito deve funcionar para outras plataformas.

Basicamente, emito dois tokens, um curto (5 minutos) e um longo que expira após 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. Ele tentará a solicitação original novamente e, se conseguiu atualizar, obtém a resposta correta, de forma transparente para o usuário. Se falhar, apenas enviará o 401 ao usuário.

Se o token curto expirou, mas ainda é autêntico e o token longo é válido e autêntico, ele atualizará o token curto usando um terminal especial no serviço que o token longo autentica (essa é a única coisa para a qual pode ser usado). 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 acesso em no máximo 5 minutos, o que é aceitável para nosso uso sem precisar armazenar uma lista negra de tokens.

Edição tardia: relendo esses meses depois que estava fresco em minha mente, devo salientar que você pode revogar o acesso ao atualizar o token curto, pois oferece uma oportunidade para chamadas mais caras (por exemplo, ligue para o banco de dados para ver se o usuário foi banido) sem pagar por isso em todas as chamadas para o seu serviço.

BytePorter
fonte
8

Abaixo estão as etapas para revogar seu token de acesso JWT:

1) Ao fazer login, envie 2 tokens (token de acesso, token de atualização) em resposta ao cliente.
2) O token de acesso terá menos tempo de validade e a atualização terá um longo prazo de validade.
3) O cliente (Front end) armazenará o token de atualização no armazenamento local e o acesso ao token em cookies.
4) O cliente usará o token de acesso para chamar apis. Porém, quando expirar, escolha o token de atualização no armazenamento local e chame a API do servidor de autenticação para obter o novo token.
5) Seu servidor de autenticação terá uma API exposta que aceitará o token de atualização e verificará sua validade e retornará um novo token de acesso.
6) Quando o token de atualização expirar, o usuário será desconectado.

Informe-me se precisar de mais detalhes. Também posso compartilhar o código (inicialização Java + Spring).

Bhupinder Singh
fonte
Você poderia compartilhar o link do seu projeto se o tiver no GitHub?
Arun Kumar N
clique aqui para o link
Bhupinder Singh
6

jwt-autorefresh

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

Essa biblioteca agenda a atualização de tokens JWT em um número calculado pelo usuário de segundos antes da expiração do token de acesso (com base na declaração exp codificada no token). Possui um amplo conjunto de testes e verifica algumas condições para garantir que qualquer atividade estranha seja acompanhada por uma mensagem descritiva sobre configurações incorretas do ambiente.

Implementação de exemplo completo

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())

aviso legal: eu sou o mantenedor

cchamberlain
fonte
Pergunta sobre isso, eu vi a função de decodificação que ele usa. Supõe que o JWT possa ser decodificado sem usar um segredo? Funciona com o JWT que foi assinado com um segredo?
Gian Franco Zabarino
3
Sim, a decodificação é uma decodificação apenas para cliente e não deve estar ciente do segredo. O segredo é usado para assinar o token JWT do lado do servidor para verificar se sua assinatura foi usada para gerar o JWT originalmente e nunca deve ser usada a partir do cliente. A mágica do JWT é que sua carga útil pode ser decodificada no lado do cliente e as declarações internas podem ser usadas para criar sua interface do usuário sem o segredo. A única coisa que o jwt-autorefreshdecodifica é extrair a expdeclaração para que possa determinar até que ponto agendar a próxima atualização.
Cchamberlain 29/09/16
1
Oh, bom saber, algo não fazia sentido, mas agora faz. Obrigado pela resposta.
Gian Franco Zabarino
4

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

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

I definir expiresInopção para o meu tempo desejado antes de o usuário será forçado a de 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 original de expiração ( 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 expiredInvalor, mas já excedeu o softexpvalor, o servidor responderá com um status separado para esse erro, por exemplo. EXPIRED_TOKEN:

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

No lado do cliente, se recebeu EXPIRED_TOKENresposta, ele deve renovar o token automaticamente enviando uma solicitação de renovação ao servidor. Isso é transparente para o usuário e cuida 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 se recusará a renovar tokens se falhar no método acima.

James A
fonte
Essa estratégia parece boa. Mas acho que deve ser complementado com uma espécie de "quantidade máxima de renovações" porque (talvez) uma sessão de usuário possa estar viva para sempre.
Juan Ignacio Barisich
1
Você pode definir uma variável hardExp nos dados do token para definir uma data máxima para forçar a expiração do token, ou talvez um contador que seja diminuído sempre que o token for renovado, limitando a quantidade total de renovações de token.
James A
1
está correto. Eu considero isso um "must".
Juan Ignacio Barisich 19/02
2

Que tal essa abordagem:

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

Não exigimos ponto final adicional para atualizar o token neste caso. Gostaria de receber qualquer feedack.

sjaiswal
fonte
É uma boa escolha hoje, parece bastante fácil de implementar.
b.ben
4
Nesse caso, onde você armazena lastAccessedTime? Você deve fazer isso no back-end e por solicitação, para que se torne uma solução com estado não desejada.
Antgar9
2

Hoje, muitas pessoas optam por fazer o gerenciamento de sessões com JWTs sem estar cientes do que estão desistindo em prol da simplicidade percebida . Minha resposta é elaborada na 2ª parte das perguntas:

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? O uso do JWT não é adequado para esse cenário?

Os JWTs são capazes de suportar o gerenciamento básico de sessões com algumas limitações. Sendo tokens auto-descritivos, eles não exigem nenhum estado no lado do servidor. Isso os torna atraentes. Por exemplo, se o serviço não tiver uma camada de persistência, ele não precisará trazer uma apenas para o gerenciamento de sessões.

No entanto, apatridia também é a principal causa de suas deficiências. Como eles são emitidos apenas uma vez com conteúdo e expiração fixos, você não pode fazer o que gostaria com uma configuração típica de gerenciamento de sessões.

Ou seja, você não pode invalidá-los sob demanda. Isso significa que você não pode implementar um logoff seguro, pois não há como expirar os tokens já emitidos. Você também não pode implementar o tempo limite ocioso pelo mesmo motivo. Uma solução é manter uma lista negra, mas isso introduz o estado.

Eu escrevi um post explicando essas desvantagens em mais detalhes. Para ficar claro, você pode contornar esses problemas adicionando mais complexidade (sessões deslizantes, tokens de atualização etc.)

Quanto a outras opções, se seus clientes interagirem apenas com o serviço por meio de um navegador, recomendo o uso de uma solução de gerenciamento de sessões baseada em cookies. Também compilei uma lista de métodos de autenticação atualmente amplamente utilizados na web.

Daniel Szpisjak
fonte