javascript - nodejs - securing node js restful apis with json web tokens
Invalidando tokens web de JSON (16)
Para un nuevo proyecto node.js en el que estoy trabajando, estoy pensando en cambiar de un enfoque de sesión basado en cookies (me refiero a 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 clave-valor) 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 comprender qué trampas / ataques comunes (o poco frecuentes) debo tener en cuenta con este tipo de paradigma. Por ejemplo, si este paradigma es vulnerable a los mismos / diferentes tipos de ataques que el enfoque de almacenamiento de sesión / basado en cookies.
Entonces, digamos que tengo lo siguiente (adaptado de this y this ):
Iniciar 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 Session Store requeriría una actualización de la base de datos KeyValueStore con el token especificado.
Parece que tal mecanismo no existiría en el enfoque basado en token, ya que el token en sí contendría la información que normalmente existiría en el almacén de clave-valor.
- Dar tiempo de caducidad de 1 día para las fichas.
- Mantenga una lista negra diaria.
- Coloque los tokens invalidados / cerrar sesión en la lista negra
Para la validación del token, compruebe primero el tiempo de caducidad del token y luego la lista negra si el token no ha caducado.
Para las necesidades de sesiones largas, debe haber un mecanismo para extender el tiempo de caducidad del token.
¿Por qué no usar simplemente el reclamo jti (nonce) y almacenarlo en una lista como 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 que presumiblemente desea obtener el registro de usuario de todos modos, y de esta manera puede tener múltiples tokens válidos para diferentes instancias de clientes ("cerrar sesión en todas partes" puede restablecer la lista a vacía)
¿Qué sucede si simplemente genera un nuevo token desde el servidor con expiresIn: 0 y lo devuelve al Cliente y lo almacena en la cookie?
Acabo de guardar el token en la tabla de usuarios, cuando el usuario inicie sesión, actualizaré el nuevo token y cuando auth sea igual al usuario jwt actual.
Creo que esta no es la mejor solución, pero que funciona para mí.
Después de 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 suceden las siguientes cosas ...
Borrar el almacenamiento / sesión del cliente
Actualice la última fecha 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 de 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 nula la fecha de cierre de sesión si el estado actual es inicio de sesión y aún no se ha desconectado)
Esto es mucho más simple que mantener una tabla adicional de lista negra y purgar regularmente. El soporte de múltiples dispositivos requiere una tabla adicional para mantener las fechas de inicio de sesión registradas y de cierre de sesión con algunos detalles adicionales como los detalles del sistema operativo o del cliente.
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 abogan por alcanzar el almacén de datos en cada solicitud. Si llega al almacén de datos principal para validar cada solicitud de autenticación, veo menos razones para usar JWT en lugar de otros mecanismos de autenticación de token establecidos. Básicamente, has hecho que JWT sea estable, 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á sin acceder al almacén de datos, lo cual es útil. Probablemente haya otros casos de uso como ese).
Dado:
No se puede lograr una autenticación JWT verdaderamente sin estado para una aplicación web típica del mundo real porque JWT sin estado no tiene una manera de proporcionar soporte inmediato y seguro para los siguientes casos de uso importantes:
La cuenta del usuario es eliminada / bloqueada / suspendida.
Se cambió la contraseña del usuario.
Se cambian los roles o permisos del usuario.
El usuario está desconectado por el administrador.
Cualquier otro dato crítico de la aplicación en el token JWT es cambiado por el administrador del sitio.
No puede esperar la caducidad del token en estos casos. La invalidación del token debe ocurrir inmediatamente. Además, no puede confiar en que el cliente no guarde y use 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.
Tiene una lista negra que contiene estos tokens hasta que se alcanza su fecha de caducidad. 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 lista negra hasta su vencimiento. Lo implementaría poniendo tokens invalidados en redis, memcached u otro almacén de datos en memoria que admita establecer un tiempo de caducidad en una clave.
Aún debe realizar 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 el conjunto de usuarios allí. (Lo que puede o no ser un gran problema para un sitio determinado).
Las ideas publicadas anteriormente son buenas, pero una forma 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) luego lo envía al cliente, simplemente cambiando el secreto invalidará todos los tokens existentes y requerirá que todos los usuarios obtengan un nuevo token para autenticarse, ya que su token antiguo de repente se vuelve inválido de acuerdo al servidor.
No requiere ninguna modificación en el contenido del token real (o ID de búsqueda).
Claramente, esto solo funciona en un caso de emergencia cuando se desea que todos los tokens existentes caduquen, ya que se requiere una de las soluciones anteriores (como el tiempo de caducidad del token corto o la invalidación de una clave almacenada dentro del token).
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 cambió 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 ha sido cambiada después de que se emitió el token y si era el token se rechaza aunque aún no haya caducado.
Lo hice de la siguiente manera:
- Genere un
unique hash
y luego guárdelo en redis y en su JWT . Esto se puede llamar una sesión- También almacenaremos la cantidad 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, se almacena en redis y se inyecta en su JWT .
Cuando un usuario intenta visitar un punto final protegido, tomará el hash de sesión único de su JWT , consultará el redis y verá si coincide.
Podemos extender esto y hacer que nuestro JWT sea aún más seguro, así es como:
Cada X solicita una JWT particular, generamos una nueva sesión única, la almacenamos en nuestra JWT y luego la lista negra de la anterior.
Esto significa que el JWT está cambiando constantemente y evita que el JWT esté siendo hackeado, robado o algo más.
Mantendría un registro del número de versión de 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 al de la versión jwt actual del usuario.
En cualquier momento que desee invalidar los archivos JWTS antiguos, simplemente ingrese el número de versión del usuario JWT.
Mantenga una lista en la 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 conserves tus tokens y de la frecuencia con que los usuarios revocan sus tokens. Use db solo cuando la tabla cambie. Cargue la tabla en la memoria cuando se inicie su aplicación.
No he probado esto todavía, y utiliza mucha información basada en algunas de las otras respuestas. La complejidad aquí es evitar una llamada del 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 db por solicitud a un almacén de sesión de usuario. Eso está bien en ciertos escenarios, pero esto se creó en un intento por evitar tales llamadas y hacer que cualquier estado requerido 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 funciones de invalidación forzada. Pero si quieres hacerlo aquí es lo esencial:
Metas:
- Mitigar el uso de un almacén de datos (sin estado).
- Posibilidad de forzar el cierre de sesión de todos los usuarios.
- Posibilidad de forzar el cierre de sesión de cualquier individuo en cualquier momento.
- Posibilidad de requerir el reingreso de la contraseña después de un cierto tiempo.
- Posibilidad de 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 va, vea los comentarios para obtener información adicional)
La solución:
- Utilice tokens de acceso de corta duración (<5 m) emparejados con un refresh-token almacenado del cliente de mayor duración (pocas horas).
- Cada solicitud comprueba la validez o la fecha de caducidad del token de actualización o 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 usuarios: si se encuentra, rechace la solicitud de actualización.
- Cuando un cliente no tiene una actualización válida (no caducada) o token de autenticación, 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, consulte el almacén de datos de usuario para la prohibición
- Al cerrar sesión: agregue a 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 desconectarlos de todos los dispositivos en un entorno de múltiples dispositivos, pero se podría hacer agregando un campo de dispositivo a 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íquela por solicitud.
- Para forzar el cierre de sesión de todos los usuarios, reinicie la clave hash de token
Esto requiere que usted mantenga una lista negra (estado) en el servidor, asumiendo que la tabla de usuarios contiene información de usuarios prohibidos. La lista negra de sesiones inválidas - es una lista de identificadores de usuario. Esta lista negra solo se verifica durante una solicitud de token de actualización. Se requieren entradas para vivir en él siempre que el token de actualización TTL. Una vez que el token de actualización caduque, el usuario deberá iniciar sesión nuevamente.
Contras:
- Aún se requiere realizar una búsqueda en el almacén de datos en la solicitud de token de actualización.
- Los tokens no válidos pueden continuar 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 el funcionamiento normal.
- Solo se requiere hacer una búsqueda en el 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 a la base de datos cada 15 minutos. Si usa reddis, almacenar una lista de sesión válida / no válida allí sería una solución muy rápida y simple. No hay necesidad de un token de actualización. Cada token de autenticación tendría un ID de sesión y un ID de dispositivo, podrían almacenarse en una tabla de reddis en el momento de la creación e invalidarse cuando sea apropiado. Luego se verificarían en cada solicitud y se rechazarían cuando fueran inválidas.
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 el usuario y pase, genere una nueva cadena aleatoria, almacénela en el campo last_key_used y agréguela a la carga útil cuando firme el token.
Cuando el usuario inicie 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 realiza un cierre de sesión, por ejemplo, o si quiere invalidar el token, simplemente cambie el campo "last_key_used" a otro valor aleatorio y cualquier comprobación posterior fallará, lo que obligará al usuario a iniciar sesión con el usuario y pasar nuevamente.
Un enfoque que he estado considerando es tener siempre un valor iat
(emitido a) en el JWT. Luego, cuando un usuario cierra la sesión, almacene esa marca de tiempo en el registro del usuario. Al validar el JWT, simplemente compare el iat
con la última marca de tiempo de iat
de sesión. Si el iat
es mayor, entonces no es válido. Sí, tienes que ir a la base de datos, pero siempre estaré retirando el registro de usuario de todos modos si el JWT es válido de otra manera.
El principal inconveniente que veo es que los desconectaría de todas sus sesiones si estuvieran en varios navegadores o también tuvieran 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 vez válida.
Yo también he estado investigando esta pregunta, y aunque ninguna de las ideas a continuación son soluciones completas, pueden ayudar a otros a descartar ideas o proporcionar otras.
1) Simplemente quite el token del cliente
Obviamente, esto no hace nada por la seguridad del lado del servidor, pero detiene a un atacante eliminando 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 fichas
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 por la que se va a utilizar un token completo, ya que necesitaría tocar la base de datos para cada solicitud. Sin embargo, el tamaño de almacenamiento probablemente sería 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 visceral, y definitivamente depende del contexto).
3) Solo mantenga cortos los tiempos de caducidad del token y gírelos a menudo
Si mantiene los tiempos de caducidad del token en intervalos suficientemente cortos, y hace que el cliente en ejecución realice un seguimiento y solicite actualizaciones cuando sea necesario, el número 1 funcionará efectivamente como un sistema de cierre de sesión completo. 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 transcurra el intervalo de caducidad).
Planes de Contingencia
Si alguna vez hubo una emergencia, o se comprometió un token de usuario, 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ía que todos los tokens asociados fueran inválidos, ya que el usuario asociado ya no podría ser encontrado.
También quise señalar que es una buena idea incluir la última fecha de inicio de sesión con el token, para que pueda imponer un inicio de sesión después de un período de tiempo lejano.
En términos de similitudes / diferencias con respecto a los ataques que usan tokens, esta publicación aborda la pregunta: http://blog.auth0.com/2014/01/07/angularjs-authentication-with-cookies-vs-token/