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.
fuente
isRevoked
opción o intentar replicar la misma funcionalidad. github.com/auth0/express-jwt#revoked-tokensRespuestas:
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
fuente
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.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).
fuente
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).
fuente
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.
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.
fuente
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:
La solución:
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:
Pros:
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.
fuente
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 eliat
con la última marca de tiempo desconectada. Siiat
es 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
iat
hora válida .fuente
token_valid_after
, o algo así. ¡Increíble!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.
fuente
if (jwt.issue_date < user.last_pw_change) { /* not valid, redirect to login */}
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.
fuente
Mantenga una lista en memoria como esta
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.
fuente
------------------------ 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 -
3) Para verificar -
4) Destruir -
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
fuente
¿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)
fuente
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.
fuente
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.
fuente
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:
por ejemplo, ver https://jwt.io (no estoy seguro de que manejen secretos dinámicos de 256 bits)
fuente
Lo hice de la siguiente manera:
unique hash
y luego almacénelo en redis y su JWT . Esto se puede llamar una sesiónEntonces, 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.
fuente
aud
yjti
reclamaciones en JWT, estás en el camino correcto.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 -
fuente
Si la opción "cerrar sesión de todos los dispositivos" es aceptable (en la mayoría de los casos lo es):
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.
fuente
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.
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
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.
fuente
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
fuente
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.
fuente
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 datosuser_enabled
es 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.fuente
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í.
fuente
Stateless JWT
yStateful JWT
(que es muy similar a las sesiones).Stateful JWT
pueden beneficiarse al mantener una lista blanca de tokens.