Invalidación de tokens web JSON

421

Para un nuevo proyecto node.js en el que estoy trabajando, estoy pensando en cambiar de un enfoque de sesión basado en cookies (con esto, quiero decir, almacenar una identificación en un almacén de valores clave que contiene sesiones de usuario en el navegador de un usuario) a un enfoque de sesión basado en token (sin almacén de valores clave) utilizando JSON Web Tokens (jwt).

El proyecto es un juego que utiliza socket.io: tener una sesión basada en token sería útil en un escenario en el que habrá múltiples canales de comunicación en una sola sesión (web y socket.io)

¿Cómo se podría proporcionar una invalidación de token / sesión desde el servidor utilizando el enfoque jwt?

También quería entender qué trampas / ataques comunes (o poco comunes) debería tener en cuenta con este tipo de paradigma. Por ejemplo, si este paradigma es vulnerable a los mismos / diferentes tipos de ataques que la tienda de sesión / enfoque basado en cookies.

Entonces, digamos que tengo lo siguiente (adaptado de esto y de esto ):

Inicio de sesión en la tienda de sesión:

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});
    });
}

Inicio de sesión basado en 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});
    });
}

-

Un cierre de sesión (o invalidar) para el enfoque de Almacén de sesión requeriría una actualización de la base de datos KeyValueStore con el token especificado.

Parece que dicho mecanismo no existiría en el enfoque basado en tokens ya que el token en sí contendría la información que normalmente existiría en el almacén de valores clave.

funseiki
fuente
1
Si está utilizando el paquete 'express-jwt', puede echar un vistazo a la isRevokedopción o intentar replicar la misma funcionalidad. github.com/auth0/express-jwt#revoked-tokens
Signus
1
Considere usar un tiempo de vencimiento corto en el token de acceso y usar un token de actualización, con un vencimiento de larga duración, para permitir verificar el estado de acceso del usuario en una base de datos (lista negra). auth0.com/blog/…
Rohmer
Otra opción sería adjuntar la dirección IP en la carga útil mientras se genera el token jwt y se verifica la IP almacenada frente a la solicitud entrante para la misma dirección IP. Ej: req.connection.remoteAddress en nodeJs. Hay proveedores de ISP que no emiten IP estática por cliente, creo que esto no será un problema a menos que un cliente se vuelva a conectar a Internet.
Gihan Sandaru

Respuestas:

392

Yo también he estado investigando esta pregunta, y aunque ninguna de las ideas a continuación son soluciones completas, podrían ayudar a otros a descartar ideas u ofrecer otras.

1) Simplemente elimine el token del cliente

Obviamente, esto no hace nada para la seguridad del lado del servidor, pero detiene a un atacante al eliminar el token de la existencia (es decir, tendrían que haber robado el token antes de cerrar sesión).

2) Crear una lista negra de tokens

Puede almacenar los tokens no válidos hasta su fecha de caducidad inicial y compararlos con las solicitudes entrantes. Sin embargo, esto parece negar la razón para usar token basado en primer lugar, ya que necesitaría tocar la base de datos para cada solicitud. Sin embargo, es probable que el tamaño de almacenamiento sea menor, ya que solo necesitaría almacenar tokens que se encontraban entre el tiempo de cierre de sesión y el tiempo de caducidad (esto es una sensación instintiva y definitivamente depende del contexto).

3) Solo mantenga cortos los tiempos de caducidad de los tokens y gírelos con frecuencia

Si mantiene los tiempos de vencimiento del token a intervalos lo suficientemente cortos y hace que el cliente en ejecución realice un seguimiento y solicite actualizaciones cuando sea necesario, el número 1 funcionaría efectivamente como un sistema completo de cierre de sesión. El problema con este método es que hace que sea imposible mantener al usuario conectado entre los cierres del código del cliente (dependiendo de cuánto tiempo realice el intervalo de caducidad).

Planes de Contingencia

Si alguna vez hubo una emergencia, o un token de usuario se vio comprometido, una cosa que podría hacer es permitir que el usuario cambie una ID de búsqueda de usuario subyacente con sus credenciales de inicio de sesión. Esto hará que todos los tokens asociados no sean válidos, ya que el usuario asociado ya no podrá ser encontrado.

También quería señalar que es una buena idea incluir la última fecha de inicio de sesión con el token, para que pueda forzar un nuevo inicio de sesión después de un período de tiempo distante.

En términos de similitudes / diferencias con respecto a los ataques que usan tokens, esta publicación aborda la pregunta: https://github.com/dentarg/blog/blob/master/_posts/2014-01-07-angularjs-authentication-with-cookies -vs-token.markdown

Matt Way
fuente
3
Excelente acercamiento. Mi instinto sería hacer una combinación de los 3 y / o solicitar un nuevo token después de cada "n" solicitudes (en lugar de un temporizador). Estamos usando redis para el almacenamiento de objetos en memoria, y podríamos usar esto fácilmente para el caso # 2, y luego la latencia iría MUCHO.
Aaron Wagner
2
Esta publicación de horror de codificación ofrece algunos consejos: mantenga breves las cookies (o tokens) de la sesión, pero hágala invisible para el usuario, lo que parece estar en línea con el n. ° 3. Mi propio instinto (tal vez porque es más tradicional) es solo que el token (o un hash de él) actúe como una clave en la base de datos de sesión listada en blanco (similar al # 2)
funseiki
8
El artículo está bien escrito y es una versión elaborada de 2)arriba. Si bien funciona bien, personalmente no veo mucha diferencia con las tiendas de sesiones tradicionales. Supongo que el requisito de almacenamiento sería menor, pero aún se requiere una base de datos. El mayor atractivo de JWT para mí fue no usar una base de datos para las sesiones.
Matt Way
211
Un enfoque común para invalidar tokens cuando un usuario cambia su contraseña es firmar el token con un hash de su contraseña. Por lo tanto, si la contraseña cambia, los tokens anteriores no se verifican automáticamente. Puede extender esto para cerrar sesión incluyendo un último tiempo de cierre de sesión en el registro del usuario y utilizando una combinación del último hash de tiempo de cierre de sesión y contraseña para firmar el token. Esto requiere una búsqueda de base de datos cada vez que necesite verificar la firma del token, pero presumiblemente está buscando al usuario de todos modos.
Travis Terry
44
Una lista negra puede hacerse eficiente manteniéndola en la memoria, de modo que la base de datos solo necesite ser golpeada para registrar invalidaciones y eliminar invalidaciones vencidas y solo leerse en el inicio del servidor. Bajo una arquitectura de equilibrio de carga, la lista negra en memoria puede sondear la base de datos a intervalos cortos, como 10 segundos, lo que limita la exposición de tokens invalidados. Estos enfoques permiten que el servidor continúe autenticando solicitudes sin accesos a la base de datos por solicitud.
Joe Lapp
86

Las ideas publicadas anteriormente son buenas, pero una manera muy simple y fácil de invalidar todos los JWT existentes es simplemente cambiar el secreto.

Si su servidor crea el JWT, lo firma con un secreto (JWS) y luego lo envía al cliente, simplemente cambiar el secreto invalidará todos los tokens existentes y requerirá que todos los usuarios obtengan un nuevo token para autenticarse ya que su antiguo token de repente se vuelve inválido. al servidor

No requiere ninguna modificación en el contenido real del token (o ID de búsqueda).

Claramente, esto solo funciona para un caso de emergencia cuando desea que todos los tokens existentes caduquen, para el vencimiento del token se requiere una de las soluciones anteriores (como un tiempo de vencimiento de token corto o invalidar una clave almacenada dentro del token).

Andy
fuente
99
Creo que este enfoque no es ideal. Si bien funciona y es ciertamente simple, imagina un caso en el que estás usando una clave pública: no querrás ir y recrear esa clave cada vez que quieras invalidar un solo token.
Signus
1
@KijanaWoodard, se puede usar un par de claves pública / privada para validar la firma como el secreto del algoritmo RS256. En el ejemplo que se muestra aquí, menciona cambiar el secreto para invalidar un JWT. Esto se puede hacer ya sea a) introduciendo una clave pública falsa que no coincide con la firma ob) generando una nueva clave pública. En esa situación, es menos que ideal.
Signus
1
@Signus - te tengo. no utiliza la clave pública como secreto, pero otros pueden confiar en la clave pública para verificar la firma.
Kijana Woodard
8
Esta es una muy mala solución. La razón principal para usar JWT es sin estado y escalas. El uso de un secreto dinámico introduce un estado. Si el servicio se agrupa en varios nodos, deberá sincronizar el secreto cada vez que se emita un token nuevo. Tendría que almacenar secretos en una base de datos u otro servicio externo, que sería simplemente reinventar la autenticación basada en cookies
Tuomas Toivonen
55
@TuomasToivonen, pero debe firmar un JWT con un secreto y poder verificar el JWT con ese mismo secreto. Por lo tanto, debe almacenar el secreto en los recursos protegidos. Si el secreto se ve comprometido, debe cambiarlo y distribuir ese cambio a cada uno de sus nodos. Los proveedores de alojamiento con agrupamiento / escalado generalmente le permiten almacenar secretos en su servicio para que la distribución de estos secretos sea fácil y confiable.
Rohmer
67

Este es principalmente un comentario largo que apoya y se basa en la respuesta de @mattway

Dado:

Algunas de las otras soluciones propuestas en esta página recomiendan acceder al almacén de datos en cada solicitud. Si presiona el almacén de datos principal para validar cada solicitud de autenticación, entonces veo menos razones para usar JWT en lugar de otros mecanismos de autenticación de token establecidos. Básicamente, has hecho JWT con estado, en lugar de sin estado si vas al almacén de datos cada vez.

(Si su sitio recibe un gran volumen de solicitudes no autorizadas, JWT las rechazaría sin acceder al almacén de datos, lo cual es útil. Probablemente haya otros casos de uso como ese).

Dado:

La autenticación JWT verdaderamente sin estado no se puede lograr para una aplicación web típica del mundo real porque JWT sin estado no tiene una forma de proporcionar soporte inmediato y seguro para los siguientes casos de uso importantes:

La cuenta del usuario se elimina / bloquea / suspende.

La contraseña del usuario ha cambiado.

Se cambian los roles o permisos del usuario.

El usuario está desconectado por el administrador.

El administrador del sitio cambia cualquier otra información crítica de la aplicación en el token JWT.

No puede esperar a que caduque el token en estos casos. La invalidación del token debe ocurrir de inmediato. Además, no puede confiar en que el cliente no conservará ni utilizará una copia del token anterior, ya sea con intención maliciosa o no.

Por lo tanto: creo que la respuesta de @ matt-way, # 2 TokenBlackList, sería la forma más eficiente de agregar el estado requerido a la autenticación basada en JWT.

Tienes una lista negra que contiene estos tokens hasta que se alcanza su fecha de vencimiento. La lista de tokens será bastante pequeña en comparación con el número total de usuarios, ya que solo tiene que mantener los tokens en la lista negra hasta su vencimiento. Lo implementaría colocando tokens invalidados en redis, memcached u otro almacén de datos en memoria que admita establecer un tiempo de caducidad en una clave.

Todavía tiene que hacer una llamada a su base de datos en memoria para cada solicitud de autenticación que pase la autenticación JWT inicial, pero no tiene que almacenar claves para todo su conjunto de usuarios allí. (Que puede o no ser un gran problema para un sitio determinado).

Ed J
fuente
15
No estoy de acuerdo con tu respuesta. Golpear una base de datos no hace nada con estado; estado de almacenamiento en su backend hace. JWT no se creó para que no tenga que acceder a la base de datos en cada solicitud. Cada aplicación principal que utiliza JWT está respaldada por una base de datos. JWT resuelve un problema completamente diferente. en.wikipedia.org/wiki/Stateless_protocol
Julian
66
@Julian, ¿puedes explicar un poco esto? ¿Qué problema realmente resuelve JWT entonces?
zero01alpha
8
@ zero01alpha Autenticación: este es el escenario más común para usar JWT. Una vez que el usuario haya iniciado sesión, cada solicitud posterior incluirá el JWT, lo que le permitirá acceder a rutas, servicios y recursos que están permitidos con ese token. Intercambio de información: los tokens web JSON son una buena forma de transmitir información de manera segura entre las partes. Debido a que los JWT pueden firmarse, puede estar seguro de que los remitentes son quienes dicen ser. Ver jwt.io/introduction
Julian
77
@Julian No estoy de acuerdo con tu desacuerdo :) JWT resuelve el problema (para los servicios) de tener la necesidad de acceder a una entidad centralizada que proporciona información de autorización para un cliente determinado. Entonces, en lugar de que el servicio A y el servicio B tengan que acceder a algún recurso para averiguar si el cliente X tiene o no permisos para hacer algo, el servicio A y B reciben un token de X que prueba sus permisos (generalmente emitidos por un tercero fiesta). De todos modos, JWT es una herramienta que ayuda a evitar un estado compartido entre los servicios de un sistema, especialmente cuando están controlados por más de un proveedor de servicios.
LIvanov
1
También de jwt.io/introduction 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.
giantas
43

Mantendría un registro del número de versión jwt en el modelo de usuario. Los nuevos tokens jwt establecerían su versión para esto.

Cuando valide el jwt, simplemente verifique que tenga un número de versión igual a la versión jwt actual de los usuarios.

Cada vez que desee invalidar jwts antiguos, simplemente coloque el número de versión jwt de los usuarios.

DaftMonk
fuente
15
Esta es una idea interesante, lo único es dónde almacenar la versión, ya que parte del propósito de los tokens es que no tiene estado y no necesita usar la base de datos. Una versión codificada dificultaría el aumento, y un número de versión en una base de datos negaría algunos de los beneficios del uso de tokens.
Stephen Smith
13
Presumiblemente ya está almacenando una identificación de usuario en su token, y luego consulta la base de datos para verificar que el usuario existe / está autorizado para acceder al punto final de la API. Por lo tanto, no está haciendo ninguna consulta de db adicional al comparar el número de versión del token jwt con el del usuario.
DaftMonk
55
Presumiblemente no debería decirlo, porque hay muchas situaciones en las que podrías usar tokens con validaciones que no tocan la base de datos. Pero creo que en este caso es difícil de evitar.
DaftMonk
11
¿Qué sucede si el usuario inicia sesión desde múltiples dispositivos? ¿Debería usarse un token en todos ellos o el inicio de sesión invalida todos los anteriores?
meeDamian
10
Estoy de acuerdo con @SergioCorrea Esto haría que JWT sea casi tan dinámico como cualquier otro mecanismo de autenticación de token.
Ed J
40

No lo he probado todavía, y utiliza mucha información basada en algunas de las otras respuestas. La complejidad aquí es evitar una llamada al almacén de datos del lado del servidor por solicitud de información del usuario. La mayoría de las otras soluciones requieren una búsqueda de base de datos por solicitud a un almacén de sesión de usuario. Eso está bien en ciertos escenarios, pero esto se creó en un intento de evitar tales llamadas y hacer que el estado del lado del servidor sea muy pequeño. Terminará recreando una sesión del lado del servidor, por pequeña que sea para proporcionar todas las características de invalidación de fuerza. Pero si quieres hacerlo aquí está la esencia:

Metas:

  • Mitigar el uso de un almacén de datos (sin estado).
  • Capacidad para forzar el cierre de sesión de todos los usuarios.
  • Capacidad para forzar el cierre de sesión de cualquier persona en cualquier momento.
  • Posibilidad de solicitar el reingreso de la contraseña después de un cierto período de tiempo.
  • Capacidad para trabajar con múltiples clientes.
  • Capacidad para forzar un reinicio de sesión cuando un usuario hace clic en cerrar sesión de un cliente en particular. (Para evitar que alguien "elimine" un token de cliente después de que el usuario se retire, vea los comentarios para obtener información adicional)

La solución:

  • Use tokens de acceso de corta duración (<5 m) emparejados con un token de actualización almacenado por el cliente de mayor duración (pocas horas) .
  • Cada solicitud verifica la validez o la fecha de vencimiento del token de actualización.
  • Cuando el token de acceso caduca, el cliente usa el token de actualización para actualizar el token de acceso.
  • Durante la comprobación del token de actualización, el servidor verifica una pequeña lista negra de identificadores de usuario; si se encuentra, rechace la solicitud de actualización.
  • Cuando un cliente no tiene un token de actualización o autenticación válido (no caducado), el usuario debe volver a iniciar sesión, ya que todas las demás solicitudes serán rechazadas.
  • En la solicitud de inicio de sesión, verifique el almacenamiento de datos del usuario para la prohibición.
  • Al cerrar sesión: agregue ese usuario a la lista negra de la sesión para que tenga que volver a iniciar sesión. Tendría que almacenar información adicional para no cerrar la sesión de todos los dispositivos en un entorno de dispositivos múltiples, pero esto podría hacerse agregando un campo de dispositivo al lista negra de usuarios.
  • Para forzar el reingreso después de x cantidad de tiempo, mantenga la última fecha de inicio de sesión en el token de autenticación y verifíquelo por solicitud.
  • Para forzar el cierre de sesión de todos los usuarios, restablezca la clave hash de token.

Esto requiere que mantenga una lista negra (estado) en el servidor, suponiendo que la tabla de usuario contenga información de usuario prohibida. La lista negra de sesiones no válidas es una lista de identificadores de usuario. Esta lista negra solo se verifica durante una solicitud de token de actualización. Las entradas son necesarias para vivir siempre que el token de actualización TTL. Una vez que el token de actualización caduca, el usuario deberá iniciar sesión nuevamente.

Contras:

  • Todavía se requiere hacer una búsqueda del almacén de datos en la solicitud de token de actualización.
  • Los tokens no válidos pueden seguir funcionando para el TTL del token de acceso.

Pros:

  • Proporciona la funcionalidad deseada.
  • La acción del token de actualización está oculta para el usuario en condiciones normales de funcionamiento.
  • Solo se requiere hacer una búsqueda del almacén de datos en las solicitudes de actualización en lugar de cada solicitud. es decir, 1 cada 15 min en lugar de 1 por segundo.
  • Minimiza el estado del lado del servidor a una lista negra muy pequeña.

Con esta solución, no se necesita un almacén de datos en memoria como reddis, al menos no para la información del usuario, ya que el servidor solo realiza una llamada db cada 15 minutos más o menos. Si usa reddis, almacenar una lista de sesión válida / inválida allí sería una solución muy rápida y simple. No es necesario un token de actualización. Cada token de autenticación tendría una identificación de sesión y una identificación de dispositivo, podrían almacenarse en una tabla reddis en la creación e invalidarse cuando sea apropiado. Luego serían verificados en cada solicitud y rechazados cuando no sean válidos.

Ashtonian
fuente
¿Qué pasa con el escenario donde una persona se levanta de una computadora para dejar que otra persona use la misma computadora? La primera persona cerrará sesión y esperará que el cierre de sesión bloquee instantáneamente a la segunda persona. Si la segunda persona es un usuario promedio, el cliente podría bloquear fácilmente al usuario eliminando el token. Pero si el segundo usuario tiene habilidades de piratería, el usuario tiene tiempo para recuperar el token aún válido para autenticarse como el primer usuario. Parece que no hay forma de evitar la necesidad de invalidar inmediatamente los tokens, sin demora.
Joe Lapp
55
O puede eliminar su JWT de la sesión / almacenamiento local o cookie.
Kamil Kiełczewski
1
Gracias @ Ashtonian. Después de hacer una extensa investigación, abandoné los JWT. A menos que haga todo lo posible para asegurar la clave secreta, o a menos que delegue en una implementación segura de OAuth, los JWT son mucho más vulnerables que las sesiones regulares. Vea mi informe completo: by.jtl.xyz/2016/06/the-unspoken-vulnerability-of-jwts.html
Joe Lapp
2
El uso de un token de actualización es la clave para permitir la inclusión en la lista negra. Gran explicación: auth0.com/blog/…
Rohmer
1
Esta me parece la mejor respuesta, ya que combina un token de acceso de corta duración con un token de actualización de larga duración que puede incluirse en la lista negra. Al cerrar sesión, el cliente debe eliminar el token de acceso para que un segundo usuario no pueda obtener acceso (aunque el token de acceso seguirá siendo válido durante unos minutos más después de cerrar sesión). @Joe Lapp dice que un hacker (segundo usuario) obtiene el token de acceso incluso después de que se haya eliminado. ¿Cómo?
M3RS
14

Un enfoque que he estado considerando es tener siempre un valor iat(emitido a) en el JWT. Luego, cuando un usuario cierre sesión, almacene esa marca de tiempo en el registro de usuario. Al validar el JWT, simplemente compare el iatcon la última marca de tiempo desconectada. Si iates mayor, entonces no es válido. Sí, tiene que ir a la base de datos, pero siempre extraeré el registro de usuario de todos modos si el JWT es válido.

El principal inconveniente que veo en esto es que los cerraría sesión en todas sus sesiones si están en varios navegadores o si también tienen un cliente móvil.

Esto también podría ser un buen mecanismo para invalidar todos los JWT en un sistema. Parte de la verificación podría ser contra una marca de tiempo global de la última iathora válida .

Brack Mo
fuente
1
¡Buen pensamiento! Resolver el problema de "un dispositivo" es hacer de esto una función de contingencia en lugar de un cierre de sesión. Almacene una fecha en el registro de usuario que invalide todos los tokens emitidos antes. Algo como token_valid_after, o algo así. ¡Increíble!
OneHoopyFrood
1
Hola @OneHoopyFrood, ¿tienes un código de ejemplo para ayudarme a entender la idea de una mejor manera? ¡Realmente aprecio tu ayuda!
alexventuraio
2
Como todas las otras soluciones propuestas, esta requiere una búsqueda en la base de datos, razón por la cual esta pregunta existe porque ¡evitar esa búsqueda es lo más importante aquí! (Rendimiento, escalabilidad). En circunstancias normales, no necesita una búsqueda de base de datos para tener los datos del usuario, ya los obtuvo del cliente.
Rob Evans el
9

Llego un poco tarde aquí, pero creo que tengo una solución decente.

Tengo una columna "last_password_change" en mi base de datos que almacena la fecha y la hora en que se modificó la contraseña por última vez. También almaceno la fecha / hora de emisión en el JWT. Al validar un token, verifico si la contraseña se ha cambiado después de que se emitió el token y si fue rechazado aunque aún no haya expirado.

Matas Kairaitis
fuente
1
¿Cómo rechazas el token? ¿Puedes mostrar un breve código de ejemplo?
alexventuraio
1
if (jwt.issue_date < user.last_pw_change) { /* not valid, redirect to login */}
Vanuan
15
Requiere una búsqueda de db!
Rob Evans el
5

Puede tener un campo "last_key_used" en su base de datos en el documento / registro de su usuario.

Cuando el usuario inicie sesión con user y pass, genere una nueva cadena aleatoria, almacénela en el campo last_key_used y agréguela a la carga útil al firmar el token.

Cuando el usuario inicia sesión con el token, verifique last_key_used en la base de datos para que coincida con el del token.

Luego, cuando el usuario cierra la sesión, por ejemplo, o si desea invalidar el token, simplemente cambie ese campo "last_key_used" a otro valor aleatorio y cualquier verificación posterior fallará, lo que obligará al usuario a iniciar sesión con el usuario y pasar de nuevo.

NickVarcha
fuente
Esta es la solución que he estado considerando, pero tiene estos inconvenientes: (1) estás haciendo una búsqueda de base de datos en cada solicitud para verificar al azar (anulando la razón para usar tokens en lugar de sesiones) o estás solo verificando de manera intermitente después de que un token de actualización haya expirado (evitando que los usuarios cierren sesión inmediatamente o que las sesiones se terminen de inmediato); y (2) cerrar sesión cierra la sesión del usuario de todos los navegadores y todos los dispositivos (lo cual no es un comportamiento convencionalmente esperado).
Joe Lapp
No necesita cambiar la clave cuando el usuario cierra la sesión, solo cuando cambia su contraseña o, si la proporciona, cuando elige cerrar sesión en todos los dispositivos
NickVarcha
3

Mantenga una lista en memoria como esta

user_id   revoke_tokens_issued_before
-------------------------------------
123       2018-07-02T15:55:33
567       2018-07-01T12:34:21

Si sus tokens caducan en una semana, limpie o ignore los registros anteriores. También mantenga solo el registro más reciente de cada usuario. El tamaño de la lista dependerá de cuánto tiempo guarde sus tokens y con qué frecuencia los usuarios revocan sus tokens. Use db solo cuando la tabla cambie. Cargue la tabla en la memoria cuando se inicie su aplicación.

Eduardo
fuente
2
La mayoría de los sitios de producción se ejecutan en más de un servidor, por lo que esta solución no funcionará. Agregar Redis o caché de interpoces similar complica significativamente el sistema y a menudo trae más problemas que soluciones.
user2555515
@ user2555515 todos los servidores pueden sincronizarse con la base de datos. Es su elección ir a la base de datos cada vez o no. Se podría decir qué problemas trae.
Eduardo
3

------------------------ Un poco tarde para esta respuesta, pero puede ser que ayude a alguien ------------- -----------

Desde el lado del cliente , la forma más fácil es eliminar el token del almacenamiento del navegador.

Pero, ¿qué pasa si quieres destruir el token en el servidor Node?

El problema con el paquete JWT es que no proporciona ningún método o forma de destruir el token. Puede usar diferentes métodos con respecto a JWT que se mencionan anteriormente. Pero aquí voy con los jwt-redis.

Entonces, para destruir el token en el lado del servidor, puede usar el paquete jwt-redis en lugar de JWT

Esta biblioteca (jwt-redis) repite por completo toda la funcionalidad de la biblioteca jsonwebtoken, con una adición importante. Jwt-redis le permite almacenar la etiqueta del token en redis para verificar la validez. La ausencia de una etiqueta de token en redis hace que el token no sea válido. Para destruir el token en jwt-redis, hay un método de destrucción

funciona de esta manera:

1) Instalar jwt-redis desde npm

2) Para crear -

var redis = require('redis');
var JWTR =  require('jwt-redis').default;
var redisClient = redis.createClient();
var jwtr = new JWTR(redisClient);

jwtr.sign(payload, secret)
    .then((token)=>{
            // your code
    })
    .catch((error)=>{
            // error handling
    });

3) Para verificar -

jwtr.verify(token, secret);

4) Destruir -

jwtr.destroy(token)

Nota : puede proporcionar expiresIn durante el inicio de sesión del token de la misma manera que se proporciona en JWT.

Puede ser que esto ayude a alguien

Aman Kumar Gupta
fuente
2

¿Por qué no usar simplemente el reclamo jti (nonce) y almacenarlo en una lista como un campo de registro de usuario (dependiente de db, pero al menos una lista separada por comas está bien)? No es necesario realizar una búsqueda por separado, ya que otros han señalado presumiblemente que desea obtener el registro de usuario de todos modos, y de esta manera puede tener múltiples tokens válidos para diferentes instancias de cliente ("cerrar sesión en todas partes" puede restablecer la lista a vacía)

davidkomer
fuente
Si esto. Tal vez haga una relación uno a muchos entre la tabla de usuario y una nueva tabla (sesión), para que pueda almacenar metadatos junto con las declaraciones jti.
Peter Lada
2
  1. Dar 1 día de tiempo de caducidad para los tokens
  2. Mantener una lista negra diaria.
  3. Ponga los tokens invalidados / de cierre de sesión en la lista negra

Para la validación del token, verifique primero el tiempo de vencimiento del token y luego la lista negra si el token no ha caducado.

Para necesidades de sesiones largas, debe haber un mecanismo para extender el tiempo de expiración del token.

Ebru Yener
fuente
44
pon tokens en la lista negra y ahí va tu apatridia
Kerem Baydogan el
2

Tarde a la fiesta, mis dos centavos se dan a continuación después de algunas investigaciones. Durante el cierre de sesión, asegúrese de que sucedan las siguientes cosas ...

Borrar el almacenamiento / sesión del cliente

Actualice la última fecha y hora de inicio de sesión de la tabla de usuario y la fecha y hora de cierre de sesión cada vez que se inicie o cierre la sesión respectivamente. Por lo tanto, la fecha de inicio de sesión siempre debe ser mayor que el cierre de sesión (o mantenga la fecha de cierre de sesión nula si el estado actual es inicio de sesión y aún no ha finalizado)

Esto es mucho más simple que mantener una tabla adicional de listas negras y purgarlas regularmente. La compatibilidad con múltiples dispositivos requiere una tabla adicional para mantener las fechas de inicio de sesión y cierre de sesión con algunos detalles adicionales, como el SO o los detalles del cliente.

Shamseer
fuente
2

Cadena única por usuario y cadena global combinada

servir como la porción secreta de JWT que permite la invalidación de tokens tanto individuales como globales. Máxima flexibilidad a costa de una búsqueda / lectura de db durante la solicitud de autenticación. También es fácil de almacenar en caché, ya que rara vez cambian.

Aquí hay un ejemplo:

HEADER:ALGORITHM & TOKEN TYPE

{
  "alg": "HS256",
  "typ": "JWT"
}
PAYLOAD:DATA

{
  "sub": "1234567890",
  "some": "data",
  "iat": 1516239022
}
VERIFY SIGNATURE

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload), 
  HMACSHA256('perUserString'+'globalString')
)

where HMACSHA256 is your local crypto sha256
  nodejs 
    import sha256 from 'crypto-js/sha256';
    sha256(message);

por ejemplo, ver https://jwt.io (no estoy seguro de que manejen secretos dinámicos de 256 bits)

Mark Essel
fuente
1
Algo más de detalle sería suficiente
giantas
2
@giantas, creo que lo que Mark quiere decir es la parte de firma. Por lo tanto, en lugar de usar una sola clave para firmar el JWT, combínelo con una clave única para cada cliente. Por lo tanto, si desea invalidar todas las sesiones de un usuario, simplemente cambie la clave para ese usuario y si invalida todas las sesiones en su sistema, simplemente cambie esa clave única global.
Tommy Aria Pradana
1

Lo hice de la siguiente manera:

  1. Genere un unique hashy luego almacénelo en redis y su JWT . Esto se puede llamar una sesión
    • También almacenaremos el número de solicitudes que ha realizado el JWT en particular : cada vez que se envía un jwt al servidor, incrementamos el número entero de solicitudes . (esto es opcional)

Entonces, cuando un usuario inicia sesión, se crea un hash único, almacenado en redis e inyectado en su JWT .

Cuando un usuario intenta visitar un punto final protegido , tomará el hash de sesión único de su JWT , consultará redis y verá si coincide.

Podemos ampliar esto y hacer que nuestro JWT sea aún más seguro, así es como:

Cada X solicita un JWT en particular , generamos una nueva sesión única, la almacenamos en nuestro JWT y luego ponemos en la lista negra la anterior.

Esto significa que el JWT está cambiando constantemente y evita que el JWT obsoleto sea ​​pirateado, robado u otra cosa.

James111
fuente
1
Puede hacer hash el token en sí y almacenar ese valor en redis, en lugar de inyectar un nuevo hash en el token.
Frug
También echa un vistazo audy jtireclamaciones en JWT, estás en el camino correcto.
Peter Lada
1

Si desea poder revocar tokens de usuario, puede realizar un seguimiento de todos los tokens emitidos en su base de datos y verificar si son válidos (existen) en una tabla similar a una sesión. La desventaja es que alcanzará la base de datos en cada solicitud.

No lo he probado, pero sugiero el siguiente método para permitir la revocación de tokens y mantener los hits de DB al mínimo:

Para reducir la tasa de verificación de la base de datos, divida todos los tokens JWT emitidos en grupos X de acuerdo con alguna asociación determinista (por ejemplo, 10 grupos por el primer dígito de la identificación del usuario).

Cada token JWT contendrá la identificación del grupo y una marca de tiempo creada al crear el token. p.ej,{ "group_id": 1, "timestamp": 1551861473716 }

El servidor mantendrá todos los identificadores de grupo en la memoria y cada grupo tendrá una marca de tiempo que indica cuándo fue el último evento de cierre de sesión de un usuario que pertenece a ese grupo. p.ej,{ "group1": 1551861473714, "group2": 1551861487293, ... }

Se verificará la validez de las solicitudes con un token JWT que tenga una marca de tiempo de grupo más antigua (hit DB) y, si es válida, se emitirá un nuevo token JWT con una nueva marca de tiempo para uso futuro del cliente. Si la marca de tiempo del grupo del token es más nueva, confiamos en el JWT (sin hit de DB).

Entonces -

  1. Solo validamos un token JWT usando la base de datos si el token tiene una marca de tiempo del grupo anterior, mientras que las solicitudes futuras no se validarán hasta que alguien en el grupo del usuario cierre la sesión.
  2. Usamos grupos para limitar la cantidad de cambios de marca de tiempo (digamos que hay un usuario que inicia y cierra sesión como si no hubiera un mañana, solo afectará a un número limitado de usuarios en lugar de a todos)
  3. Limitamos la cantidad de grupos para limitar la cantidad de marcas de tiempo almacenadas en la memoria
  4. Invalidar un token es muy sencillo: simplemente elimínelo de la tabla de sesión y genere una nueva marca de tiempo para el grupo de usuarios.
Arik
fuente
La misma lista puede mantenerse en la memoria (aplicación para c #) y eliminaría la necesidad de presionar el db para cada solicitud. La lista se puede cargar desde db al inicio de la aplicación
dvdmn
1

Si la opción "cerrar sesión de todos los dispositivos" es aceptable (en la mayoría de los casos lo es):

  • Agregue el campo de versión de token al registro de usuario.
  • Agregue el valor en este campo a los reclamos almacenados en el JWT.
  • Incremente la versión cada vez que el usuario cierre sesión.
  • Al validar el token, compare su reclamo de versión con la versión almacenada en el registro de usuario y rechace si no es lo mismo.

De todos modos, se requiere un viaje de db para obtener el registro de usuario en la mayoría de los casos, por lo que no agrega mucha sobrecarga al proceso de validación. A diferencia de mantener una lista negra, donde la carga de la base de datos es importante debido a la necesidad de utilizar una combinación o una llamada separada, limpiar registros antiguos, etc.

usuario2555515
fuente
0

Voy a responder si necesitamos proporcionar la función de cierre de sesión de todos los dispositivos cuando usamos JWT. Este enfoque utilizará búsquedas en la base de datos para cada solicitud. Porque necesitamos un estado de seguridad persistente incluso si hay un bloqueo del servidor. En la tabla de usuarios tendremos dos columnas.

  1. LastValidTime (predeterminado: tiempo de creación)
  2. Inició sesión (predeterminado: verdadero)

Siempre que haya una solicitud de cierre de sesión del usuario, actualizaremos LastValidTime a la hora actual y Logged-In a false. Si hay una solicitud de inicio de sesión, no cambiaremos LastValidTime pero Logged-In se establecerá en true.

Cuando creamos el JWT tendremos el tiempo de creación de JWT en la carga útil. Cuando autoricemos un servicio, verificaremos 3 condiciones

  1. ¿Es válido JWT?
  2. ¿El tiempo de creación de carga JWT es mayor que el Usuario LastValidTime?
  3. ¿El usuario ha iniciado sesión?

Veamos un escenario práctico.

El usuario X tiene dos dispositivos A, B. Ingresó a nuestro servidor a las 7 pm usando el dispositivo A y el dispositivo B. (digamos que el tiempo de caducidad de JWT es de 12 horas). A y B tienen JWT con createdTime: 7pm

A las 9 pm perdió su dispositivo B. Inmediatamente se desconectó del dispositivo A. Eso significa que ahora nuestra entrada de usuario X de la base de datos tiene LastValidTime como "ThatDate: 9: 00: xx: xxx" e inició sesión como "false".

A las 9:30, Mr.Thief intenta iniciar sesión utilizando el dispositivo B. Comprobaremos la base de datos, incluso si Logged-In es falso, por lo que no lo permitiremos.

A las 10 pm Mr.X inicia sesión desde su dispositivo A. Ahora el dispositivo A tiene JWT con la hora creada: 10pm. Ahora la base de datos iniciada sesión está establecida en "verdadero"

A las 10:30 p. M., Mr.Thief intenta iniciar sesión. A pesar de que Logged-In es cierto. LastValidTime es las 9 p.m. en la base de datos, pero el JWT de B ha creado la hora a las 7 p.m. Por lo tanto, no se le permitirá acceder al servicio. Entonces, al usar el dispositivo B sin tener la contraseña, no puede usar el JWT ya creado después de que un dispositivo cierre la sesión.

Tharsanan
fuente
0

La solución IAM como Keycloak (en la que he trabajado) proporciona un punto final de revocación de tokens como

Punto final de revocación de token /realms/{realm-name}/protocol/openid-connect/revoke

Si simplemente desea cerrar la sesión de un agente de uso (o usuario), también podría llamar a un punto final (esto simplemente invalidaría los tokens). Nuevamente, en el caso de Keycloak, Relying Party solo necesita llamar al punto final

/realms/{realm-name}/protocol/openid-connect/logout

Enlace en caso de que quiera aprender más

Subbu Mahadevan
fuente
-1

Esto parece realmente difícil de resolver sin una búsqueda de DB en cada verificación de token. La alternativa que se me ocurre es mantener una lista negra de tokens invalidados del lado del servidor; que debe actualizarse en una base de datos cada vez que ocurre un cambio para persistir los cambios entre reinicios, haciendo que el servidor verifique la base de datos al reiniciar para cargar la lista negra actual.

Pero si lo mantiene en la memoria del servidor (una especie de variable global), entonces no será escalable en varios servidores si está utilizando más de uno, por lo que en ese caso puede mantenerlo en un caché de Redis compartido, que debería ser configurar para conservar los datos en alguna parte (¿base de datos? ¿sistema de archivos?) en caso de que deba reiniciarse, y cada vez que se activa un nuevo servidor, debe suscribirse al caché de Redis.

Alternativa a una lista negra, con la misma solución, puede hacerlo con un hash guardado en redis por sesión como señala esta otra respuesta (aunque no estoy seguro de que sería más eficiente si muchos usuarios inician sesión ).

¿Suena terriblemente complicado? me hace!

Descargo de responsabilidad: no he usado Redis.

Jose PV
fuente
-1

Si está utilizando axios o una lib de solicitud http basada en promesas similar, simplemente puede destruir el token en el front-end dentro de la .then()parte. Se iniciará en la parte de respuesta .then () después de que el usuario ejecute esta función (el código de resultado del punto final del servidor debe estar bien, 200). Después de que el usuario hace clic en esta ruta mientras busca datos, si el campo de la base de datos user_enabledes falso, se activará la destrucción del token y el usuario se desconectará inmediatamente y se le impedirá acceder a las rutas / páginas protegidas. No tenemos que esperar a que el token caduque mientras el usuario está conectado permanentemente.

function searchForData() {   // front-end js function, user searches for the data
    // protected route, token that is sent along http request for verification
    var validToken = 'Bearer ' + whereYouStoredToken; // token stored in the browser 

    // route will trigger destroying token when user clicks and executes this func
    axios.post('/my-data', {headers: {'Authorization': validToken}})
     .then((response) => {
   // If Admin set user_enabled in the db as false, we destroy token in the browser localStorage
       if (response.data.user_enabled === false) {  // user_enabled is field in the db
           window.localStorage.clear();  // we destroy token and other credentials
       }  
    });
     .catch((e) => {
       console.log(e);
    });
}
Dan
fuente
-3

Solo guardo el token en la tabla de usuarios, cuando el inicio de sesión del usuario actualizaré el nuevo token y cuando la autenticación sea igual al jwt actual del usuario.

Creo que esta no es la mejor solución, pero eso funciona para mí.

Vo Manh Kien
fuente
2
¡Por supuesto que no es lo mejor! Cualquier persona que tenga acceso a la base de datos puede hacerse pasar fácilmente por cualquier usuario.
user2555515
1
@ user2555515 Esta solución funciona bien si el token que está almacenado en la base de datos está encriptado, como debería estarlo cualquier contraseña almacenada en la base de datos. Hay una diferencia entre Stateless JWTy Stateful JWT(que es muy similar a las sesiones). Stateful JWTpueden beneficiarse al mantener una lista blanca de tokens.
TheDarkIn1978