Para um novo projeto node.js em que estou trabalhando, estou pensando em mudar de uma abordagem de sessão baseada em cookie (com isso, quero dizer, armazenar um ID em um armazenamento de valores-chave que contém sessões do usuário no navegador do usuário) para uma abordagem de sessão baseada em token (sem armazenamento de valores-chave) usando JSON Web Tokens (jwt).
O projeto é um jogo que utiliza o socket.io - ter uma sessão baseada em token seria útil em um cenário em que haverá vários canais de comunicação em uma única sessão (web e socket.io)
Como fornecer uma invalidação de token / sessão do servidor usando a abordagem jwt?
Eu também queria entender que armadilhas / ataques comuns (ou incomuns) eu deveria procurar com esse tipo de paradigma. Por exemplo, se esse paradigma estiver vulnerável aos mesmos / diferentes tipos de ataques que a abordagem de armazenamento de sessão / cookie.
Então, digamos que tenho o seguinte (adaptado disso e deste ):
Login da Loja de Sessão:
app.get('/login', function(request, response) {
var user = {username: request.body.username, password: request.body.password };
// Validate somehow
validate(user, function(isValid, profile) {
// Create session token
var token= createSessionToken();
// Add to a key-value database
KeyValueStore.add({token: {userid: profile.id, expiresInMinutes: 60}});
// The client should save this session token in a cookie
response.json({sessionToken: token});
});
}
Login baseado em token:
var jwt = require('jsonwebtoken');
app.get('/login', function(request, response) {
var user = {username: request.body.username, password: request.body.password };
// Validate somehow
validate(user, function(isValid, profile) {
var token = jwt.sign(profile, 'My Super Secret', {expiresInMinutes: 60});
response.json({token: token});
});
}
-
Um logout (ou invalidação) da abordagem de Armazenamento de Sessão exigiria uma atualização no banco de dados KeyValueStore com o token especificado.
Parece que esse mecanismo não existiria na abordagem baseada em token, pois o próprio token conteria as informações que normalmente existiriam no armazenamento de valores-chave.
fonte
isRevoked
opção ou tentar replicar a mesma funcionalidade. github.com/auth0/express-jwt#revoked-tokensRespostas:
Eu também tenho pesquisado essa questão e, embora nenhuma das idéias abaixo seja uma solução completa, elas podem ajudar outras pessoas a descartar idéias ou fornecer outras.
1) Simplesmente remova o token do cliente
Obviamente, isso não faz nada pela segurança do servidor, mas impede um invasor removendo o token da existência (ou seja, eles teriam que ter roubado o token antes do logout).
2) Crie uma lista negra de tokens
Você pode armazenar os tokens inválidos até a data de vencimento inicial e compará-los com as solicitações recebidas. No entanto, isso parece negar o motivo de obter um token completo, pois você precisaria tocar no banco de dados para cada solicitação. O tamanho do armazenamento provavelmente seria menor, pois você só precisaria armazenar tokens que estavam entre o logout e o tempo de expiração (esse é um pressentimento e definitivamente depende do contexto).
3) Apenas mantenha curtos os prazos de validade dos tokens e gire-os com frequência
Se você mantiver os tempos de expiração do token em intervalos curtos o suficiente e pedir ao cliente em execução que acompanhe e solicite atualizações quando necessário, o número 1 funcionaria efetivamente como um sistema de logout completo. O problema com esse método é que torna impossível manter o usuário conectado entre os fechamentos do código do cliente (dependendo de quanto tempo você faz o intervalo de expiração).
Planos de contingência
Se alguma vez houve uma emergência ou um token de usuário foi comprometido, uma coisa que você pode fazer é permitir que o usuário altere um ID de pesquisa de usuário subjacente com suas credenciais de logon. Isso tornaria todos os tokens associados inválidos, pois o usuário associado não poderia mais ser encontrado.
Também gostaria de observar que é uma boa ideia incluir a data do último login com o token, para que você possa impor um novo logon após um período distante.
Em termos de semelhanças / diferenças com relação a ataques usando tokens, esta postagem aborda a questão: https://github.com/dentarg/blog/blob/master/_posts/2014-01-07-angularjs-authentication-with-cookies -vs-token.markdown
fonte
2)
acima. Embora funcione bem, pessoalmente não vejo muita diferença nas lojas de sessões tradicionais. Eu acho que o requisito de armazenamento seria menor, mas você ainda precisa de um banco de dados. O maior apelo do JWT para mim foi não usar um banco de dados para sessões.As idéias postadas acima são boas, mas uma maneira muito simples e fácil de invalidar todas as JWTs existentes é simplesmente mudar o segredo.
Se o servidor criar o JWT, assiná-lo com um segredo (JWS) e enviá-lo ao cliente, simplesmente alterar o segredo invalidará todos os tokens existentes e exigirá que todos os usuários obtenham um novo token para autenticar, pois seu token antigo se torna subitamente inválido de acordo com para o servidor.
Não requer nenhuma modificação no conteúdo real do token (ou ID da pesquisa).
Claramente, isso só funciona para um caso de emergência quando você deseja que todos os tokens existentes expirem. Para expirar por token, é necessária uma das soluções acima (como tempo de expiração curto do token ou invalidar uma chave armazenada dentro do token).
fonte
Este é principalmente um longo comentário que apóia e desenvolve a resposta de @mattway
Dado:
Algumas das outras soluções propostas nesta página defendem que o armazenamento de dados seja atingido em todas as solicitações. Se você acessar o armazenamento de dados principal para validar todas as solicitações de autenticação, vejo menos motivos para usar o JWT em vez de outros mecanismos de autenticação de token estabelecidos. Você essencialmente tornou o JWT com estado, em vez de sem estado, se você for ao armazenamento de dados todas as vezes.
(Se o seu site receber um grande volume de solicitações não autorizadas, a JWT as negará sem acessar o armazenamento de dados, o que é útil. Provavelmente existem outros casos de uso como esse.)
Dado:
A autenticação JWT verdadeiramente sem estado não pode ser alcançada para um aplicativo Web típico do mundo real, porque o JWT sem estado não tem uma maneira de fornecer segurança imediata e segura suporte para os seguintes casos de uso importantes:
A conta do usuário é excluída / bloqueada / suspensa.
A senha do usuário foi alterada.
As funções ou permissões do usuário são alteradas.
Usuário desconectado pelo administrador.
Quaisquer outros dados críticos do aplicativo no token JWT são alterados pelo administrador do site.
Você não pode esperar pela expiração do token nesses casos. A invalidação do token deve ocorrer imediatamente. Além disso, você não pode confiar no cliente para não manter e usar uma cópia do token antigo, com intenção maliciosa ou não.
Portanto: acho que a resposta de @ matt-way, # 2 TokenBlackList, seria a maneira mais eficiente de adicionar o estado necessário à autenticação baseada em JWT.
Você tem uma lista negra que contém esses tokens até a data de vencimento. A lista de tokens será muito pequena em comparação com o número total de usuários, pois ele só precisa manter os tokens na lista negra até sua expiração. Eu implementaria colocando tokens inválidos em redis, memcached ou outro armazenamento de dados na memória que suporta a definição de um tempo de expiração em uma chave.
Você ainda precisa fazer uma chamada para o banco de dados na memória para cada solicitação de autenticação que passa a autenticação JWT inicial, mas não precisa armazenar chaves para todo o conjunto de usuários. (O que pode ou não ser importante para um determinado site.)
fonte
If the JWT contains the necessary data, the need to query the database for certain operations may be reduced, though this may not always be the case.
Eu manteria um registro do número da versão do jwt no modelo do usuário. Os novos tokens jwt definiriam sua versão para isso.
Ao validar o jwt, basta verificar se ele possui um número de versão igual à versão atual do jwt dos usuários.
Sempre que você desejar invalidar jwts antigos, basta aumentar o número da versão do jwt dos usuários.
fonte
Ainda não tentei isso, e usa muitas informações com base em algumas das outras respostas. A complexidade aqui é evitar uma chamada de armazenamento de dados no servidor por solicitação de informações do usuário. A maioria das outras soluções requer uma pesquisa de banco de dados por solicitação em um armazenamento de sessão do usuário. Isso é bom em certos cenários, mas isso foi criado na tentativa de evitar essas chamadas e tornar o estado do lado do servidor necessário muito pequeno. Você acabará recriando uma sessão do lado do servidor, por menor que seja, para fornecer todos os recursos de invalidação de força. Mas se você quiser fazer isso aqui está a essência:
Metas:
A solução:
Isso requer que você mantenha uma lista negra (estado) no servidor, supondo que a tabela do usuário contenha informações banidas do usuário. A lista negra de sessões inválidas - é uma lista de IDs de usuário. Essa lista negra é verificada apenas durante uma solicitação de atualização de token. As entradas são necessárias para permanecer nele, desde que o token de atualização TTL. Depois que o token de atualização expirar, o usuário deverá fazer login novamente.
Contras:
Prós:
Com esta solução, não é necessário um armazenamento de dados na memória como reddis, pelo menos não para informações do usuário, pois você é o servidor que faz apenas uma chamada db a cada 15 minutos. Se estiver usando reddis, armazenar uma lista de sessões válida / inválida seria uma solução muito mais rápida e simples. Não há necessidade de um token de atualização. Cada token de autenticação teria uma identificação de sessão e um dispositivo, eles poderiam ser armazenados em uma tabela reddis na criação e invalidados quando apropriado. Eles seriam verificados em cada solicitação e rejeitados quando inválidos.
fonte
Uma abordagem que tenho considerado é sempre ter um valor
iat
(emitido em) no JWT. Em seguida, quando um usuário fizer logoff, armazene esse carimbo de data / hora no registro do usuário. Ao validar o JWT, basta comparar oiat
registro de data e hora com o último logoff. Se oiat
for mais antigo, não é válido. Sim, você precisa acessar o banco de dados, mas sempre processarei o registro do usuário se o JWT for válido.A principal desvantagem que vejo disso é que os desconectaria de todas as sessões se estiverem em vários navegadores ou também tiver um cliente móvel.
Esse também pode ser um bom mecanismo para invalidar todas as JWTs em um sistema. Parte da verificação pode ser contra um registro de data e hora global do último
iat
horário válido .fonte
token_valid_after
, ou algo assim. Impressionante!Estou um pouco atrasado aqui, mas acho que tenho uma solução decente.
Eu tenho uma coluna "last_password_change" no meu banco de dados que armazena a data e a hora em que a senha foi alterada pela última vez. Também guardo a data / hora da emissão no JWT. Ao validar um token, verifico se a senha foi alterada após a emissão do token e se o token foi rejeitado, mesmo que ainda não tenha expirado.
fonte
if (jwt.issue_date < user.last_pw_change) { /* not valid, redirect to login */}
Você pode ter um campo "last_key_used" no seu banco de dados no documento / registro do usuário.
Quando o usuário efetua login com user e passa, gere uma nova sequência aleatória, armazene-a no campo last_key_used e adicione-a à carga útil ao assinar o token.
Quando o usuário efetua login usando o token, verifique o last_key_used no banco de dados para corresponder ao do token.
Então, quando o usuário fizer um logout, por exemplo, ou se você desejar invalidar o token, basta alterar o campo "last_key_used" para outro valor aleatório e qualquer verificação subsequente falhará, forçando o usuário a efetuar login com o usuário e passar novamente.
fonte
Mantenha uma lista na memória como esta
Se seus tokens expirarem em uma semana, limpe ou ignore os registros anteriores. Mantenha também apenas o registro mais recente de cada usuário. O tamanho da lista dependerá de quanto tempo você mantém seus tokens e com que frequência os usuários os revogam. Use db somente quando a tabela mudar. Carregue a tabela na memória quando o aplicativo iniciar.
fonte
------------------------ Tarde demais para esta resposta, mas pode ser que isso ajude alguém ------------- -----------
No lado do cliente , a maneira mais fácil é remover o token do armazenamento do navegador.
Mas, e se você quiser destruir o token no servidor Node -
O problema com o pacote JWT é que ele não fornece nenhum método ou maneira de destruir o token. Você pode usar métodos diferentes em relação ao JWT mencionados acima. Mas aqui vou eu com o jwt-redis.
Portanto, para destruir o token no lado do servidor, você pode usar o pacote jwt-redis em vez do JWT
Essa biblioteca (jwt-redis) repete completamente toda a funcionalidade da biblioteca jsonwebtoken, com uma adição importante. Jwt-redis permite armazenar o rótulo do token em redis para verificar a validade. A ausência de um rótulo de token em redis torna o token inválido. Para destruir o token no jwt-redis, existe um método de destruição
funciona desta maneira:
1) Instale o jwt-redis a partir do npm
2) Para criar -
3) Para verificar -
4) Destruir -
Nota : você pode fornecer expiresIn durante o logon do token da mesma forma que é fornecido no JWT.
Pode ser que isso ajude alguém
fonte
Por que não usar apenas a declaração jti (nonce) e armazená-la em uma lista como um campo de registro do usuário (dependente de db, mas pelo menos uma lista separada por vírgula é adequada)? Não há necessidade de pesquisa separada, pois outros indicaram que você deseja obter o registro do usuário de qualquer maneira e, dessa forma, você pode ter vários tokens válidos para diferentes instâncias do cliente ("logout em qualquer lugar" pode redefinir a lista para vazia)
fonte
Para validação de token, verifique primeiro o tempo de expiração do token e, em seguida, a lista negra, se o token não tiver expirado.
Para necessidades de sessões longas, deve haver um mecanismo para estender o tempo de expiração do token.
fonte
Tarde para a festa, MEUS dois centavos são dados abaixo depois de algumas pesquisas. Durante o logout, verifique se as seguintes coisas estão acontecendo ...
Limpe o armazenamento / sessão do cliente
Atualize a data e o horário do último logon da tabela do usuário sempre que o logon ou logout ocorrer, respectivamente. Portanto, a hora da data de login sempre deve ser maior que o logout (ou mantenha a data de logout nula se o status atual for logon e ainda não estiver logado)
Isso é muito simples do que manter uma tabela adicional de lista negra e limpar regularmente. O suporte a vários dispositivos requer uma tabela adicional para manter o logon, as datas de logout com alguns detalhes adicionais, como detalhes do SO ou do cliente.
fonte
Cadeia de caracteres exclusiva por usuário e cadeia de caracteres global agrupadas
servir como parte secreta da JWT permite a invalidação de token individual e global. Flexibilidade máxima ao custo de uma pesquisa / leitura de banco de dados durante a autenticação de solicitação. Também é fácil de armazenar em cache, pois raramente mudam.Aqui está um exemplo:
por exemplo, use https://jwt.io (não tenho certeza de que eles lidam com segredos dinâmicos de 256 bits)
fonte
Fiz da seguinte maneira:
unique hash
e, em seguida, armazene-o em redis e em seu JWT . Isso pode ser chamado de sessãoPortanto, quando um usuário efetua login, um hash exclusivo é criado, armazenado em redis e injetado no seu JWT .
Quando um usuário tenta visitar um ponto de extremidade protegido, você pega o hash de sessão exclusivo do seu JWT , consulta os redis e verifica se é uma correspondência!
Podemos estender isso e tornar nosso JWT ainda mais seguro, eis como:
Todo X solicita que um JWT específico tenha feito, geramos uma nova sessão exclusiva, armazenamos em nosso JWT e, em seguida , colocamos na lista negra a anterior.
Isso significa que o JWT está mudando constantemente e impede que o antigo JWT seja invadido, roubado ou qualquer outra coisa.
fonte
aud
ejti
solicite no JWT que você está no caminho certo.Se você deseja revogar tokens de usuário, pode acompanhar todos os tokens emitidos no seu banco de dados e verificar se eles são válidos (existem) em uma tabela do tipo sessão. A desvantagem é que você atingirá o banco de dados em todas as solicitações.
Eu não tentei, mas sugiro o método a seguir para permitir a revogação de token, mantendo o mínimo de acessos ao banco de dados -
Para diminuir a taxa de verificação do banco de dados, divida todos os tokens JWT emitidos em grupos X de acordo com alguma associação determinística (por exemplo, 10 grupos pelo primeiro dígito do ID do usuário).
Cada token JWT manterá o ID do grupo e um carimbo de data e hora criado na criação do token. por exemplo,
{ "group_id": 1, "timestamp": 1551861473716 }
O servidor manterá todos os IDs de grupo na memória e cada grupo terá um registro de data e hora que indica quando foi o último evento de logoff de um usuário pertencente a esse grupo. por exemplo,
{ "group1": 1551861473714, "group2": 1551861487293, ... }
Solicitações com um token JWT que possuem um registro de data e hora de grupo mais antigo, serão verificadas quanto à validade (ocorrência do banco de dados) e, se válidas, um novo token JWT com um registro de data e hora novo será emitido para uso futuro do cliente. Se o registro de data e hora do grupo do token for mais recente, confiamos no JWT (sem ocorrência de banco de dados).
Assim -
fonte
Se a opção "sair de todos os dispositivos" for aceitável (na maioria dos casos, é):
De qualquer maneira, é necessária uma viagem de banco de dados para obter o registro do usuário na maioria dos casos, para que isso não adicione muita sobrecarga ao processo de validação. Ao contrário da manutenção de uma lista negra, em que a carga do banco de dados é significativa devido à necessidade de usar uma ligação ou uma chamada separada, limpe os registros antigos e assim por diante.
fonte
Vou responder Se precisarmos fornecer logout de todos os dispositivos quando estivermos usando o JWT. Essa abordagem usará pesquisas de banco de dados para cada solicitação. Porque precisamos de um estado de segurança persistente, mesmo se houver uma falha no servidor. Na tabela de usuários, teremos duas colunas
Sempre que houver uma solicitação de logout do usuário, atualizaremos LastValidTime para o horário atual e Logon-In para false. Se houver uma solicitação de logon, não alteraremos LastValidTime, mas o Logon-In será definido como true.
Quando criamos o JWT, teremos o tempo de criação do JWT na carga útil. Quando autorizamos um serviço, verificamos 3 condições
Vamos ver um cenário prático.
O usuário X possui dois dispositivos A, B. Ele fez login no nosso servidor às 19:00, usando o dispositivo A e o dispositivo B. (digamos que o tempo de expiração da JWT seja de 12 horas). A e B têm JWT com createdTime: 19:00
Às 21:00, ele perdeu o dispositivo B. Ele saiu imediatamente do dispositivo A. Isso significa que agora a entrada de usuário do banco de dados X tem LastValidTime como "ThatDate: 9: 00: xx: xxx" e o logon como "false".
Às 9:30, o Sr.Thief tenta efetuar login usando o dispositivo B. Verificaremos o banco de dados, mesmo que o Logon-In seja falso, portanto não permitiremos.
Às 22:00, o Sr.X efetua login no dispositivo A. Agora, o dispositivo A tem JWT com horário criado: 22:00. Agora o banco de dados conectado está definido como "true"
Às 22:30, o Sr. Thief tenta fazer login. Mesmo que o logon seja verdadeiro. O LastValidTime é 21:00 no banco de dados, mas o JWT de B criou a hora como 19:00. Portanto, ele não poderá acessar o serviço. Portanto, usando o dispositivo B sem ter a senha, ele não pode usar o JWT já criado após o logoff de um dispositivo.
fonte
A solução do IAM, como o Keycloak (na qual trabalhei), fornece um ponto de extremidade de revogação de token, como
Ponto final de revogação de token
/realms/{realm-name}/protocol/openid-connect/revoke
Se você quiser simplesmente desconectar um agente de usuário (ou usuário), também poderá chamar um ponto de extremidade (isso simplesmente invalidaria os Tokens). Novamente, no caso de Keycloak, o Terceiro de Confiança só precisa chamar o terminal
/realms/{realm-name}/protocol/openid-connect/logout
Link no caso, se você quiser saber mais
fonte
Isso parece realmente difícil de resolver sem uma consulta ao banco de dados em todas as verificações de token. A alternativa em que posso pensar é manter uma lista negra de tokens inválidos no lado do servidor; que deve ser atualizado em um banco de dados sempre que uma alteração persistir durante as reinicializações, fazendo o servidor verificar o banco de dados ao reiniciar para carregar a lista negra atual.
Mas se você o mantiver na memória do servidor (uma variável global de tipos), não será escalável em vários servidores se você estiver usando mais de um, portanto, nesse caso, você pode mantê-lo em um cache Redis compartilhado, que deve ser configuração para manter os dados em algum lugar (banco de dados? sistema de arquivos?), caso precise ser reiniciado, e toda vez que um novo servidor é ativado, é necessário assinar o cache do Redis.
Alternativa a uma lista negra, usando a mesma solução, você pode fazê-lo com um hash salvo em redis por sessão, como aponta essa outra resposta (sem ter certeza de que seria mais eficiente com muitos usuários que efetuam login).
Isso soa muito complicado? faz comigo!
Disclaimer: Eu não usei Redis.
fonte
Se você estiver usando axios ou uma lib de solicitação de http baseada em promessa semelhante, poderá simplesmente destruir o token no front-end dentro da
.then()
peça. Ele será iniciado na parte resposta .then () após o usuário executar esta função (o código de resultado do terminal do servidor deve estar ok, 200). Depois que o usuário clica nessa rota durante a pesquisa de dados, se o campo do banco de dadosuser_enabled
for falso, o token de destruição será acionado e o usuário será desconectado imediatamente e impedido de acessar rotas / páginas protegidas. Não precisamos esperar que o token expire enquanto o usuário estiver permanentemente conectado.fonte
Acabei de salvar o token na tabela users, quando o login do usuário atualizarei um novo token e quando auth for igual ao jwt atual do usuário.
Eu acho que essa não é a melhor solução, mas funciona para mim.
fonte
Stateless JWT
eStateful JWT
(que é muito semelhante às sessões).Stateful JWT
pode se beneficiar da manutenção de uma lista de permissões de tokens.