JWT (JSON Web Token) prolongación automática de caducidad

509

Me gustaría implementar la autenticación basada en JWT en nuestra nueva API REST. Pero dado que la caducidad se establece en el token, ¿es posible prolongarlo automáticamente? No quiero que los usuarios necesiten iniciar sesión después de cada X minutos si estaban usando activamente la aplicación en ese período. Eso sería un gran error de UX.

Pero prolongar la caducidad crea un nuevo token (y el antiguo aún es válido hasta que caduque). Y generar una nueva ficha después de cada solicitud me parece una tontería. Suena como un problema de seguridad cuando más de un token es válido al mismo tiempo. Por supuesto, podría invalidar el viejo usado usando una lista negra, pero necesitaría almacenar los tokens. Y uno de los beneficios de JWT es que no hay almacenamiento.

Encontré cómo Auth0 lo resolvió. Utilizan no solo el token JWT sino también un token de actualización: https://docs.auth0.com/refresh-token

Pero, de nuevo, para implementar esto (sin Auth0) necesitaría almacenar tokens de actualización y mantener su vencimiento. ¿Cuál es el beneficio real entonces? ¿Por qué no tener solo un token (no JWT) y mantener la caducidad en el servidor?

¿Hay otras opciones? ¿Usar JWT no es adecuado para este escenario?

maryo
fuente
1
En realidad, probablemente no haya problemas de seguridad con muchos tokens válidos a la vez ... En realidad, hay un número infinito de tokens válidos ... Entonces, ¿por qué tener un token de actualización entonces? Los regeneraré después de cada solicitud, en realidad no debería ser un problema.
maryo
1
Para SPA, consulte
wong2
2
@maryo Creo que tener (potencialmente) cientos o miles de JWT válidos no utilizados en cualquier momento dado aumenta su huella de ataque y es un riesgo de seguridad. En mi opinión, los JWT deben emitirse con cuidado ya que son tokens de acceso con las llaves del castillo de una manera.
java-addict301

Respuestas:

590

Trabajo en Auth0 y participé en el diseño de la función de token de actualización.

Todo depende del tipo de aplicación y aquí está nuestro enfoque recomendado.

aplicaciones web

Un buen patrón es actualizar el token antes de que caduque.

Establezca la caducidad del token en una semana y actualice el token cada vez que el usuario abra la aplicación web y cada una hora. Si un usuario no abre la aplicación durante más de una semana, tendrá que iniciar sesión nuevamente y esta es una aplicación web aceptable UX.

Para actualizar el token, su API necesita un nuevo punto final que reciba un JWT válido, no caducado y que devuelva el mismo JWT firmado con el nuevo campo de caducidad. Luego, la aplicación web almacenará el token en algún lugar.

Aplicaciones móviles / nativas

La mayoría de las aplicaciones nativas inician sesión una vez y solo una vez.

La idea es que el token de actualización nunca caduca y se puede intercambiar siempre por un JWT válido.

El problema con un token que nunca caduca es que nunca significa nunca. ¿Qué haces si pierdes tu teléfono? Por lo tanto, debe ser identificable por el usuario de alguna manera y la aplicación debe proporcionar una forma de revocar el acceso. Decidimos usar el nombre del dispositivo, por ejemplo, "iPad de Maryo". Luego, el usuario puede ir a la aplicación y revocar el acceso al "iPad de maryo".

Otro enfoque es revocar el token de actualización en eventos específicos. Un evento interesante es cambiar la contraseña.

Creemos que JWT no es útil para estos casos de uso, por lo que utilizamos una cadena generada al azar y la almacenamos de nuestro lado.

José F. Romaniello
fuente
42
Para el enfoque recomendado de aplicaciones web, si el token es válido durante una semana, ¿no nos preocupa que alguien lo intercepte y luego pueda usarlo durante tanto tiempo? Descargo de responsabilidad: no sé de qué estoy hablando.
usuario12121234
30
@wbeange sí, la intercepción es un problema, incluso con las cookies. Deberías usar https.
José F. Romaniello
15
@ JoséF.Romaniello En su ejemplo de aplicación web, todo tiene sentido para mí excepto tener que almacenar el token. Pensé que la belleza de JWT era la autenticación sin estado, lo que significa que la aplicación web NO tiene que almacenar el token tal como está firmado. Creo que el servidor podría simplemente verificar la validez del token, asegurarse de que esté dentro del período de vencimiento y luego emitir un token JWT renovado. ¿Podría por favor elaborar sobre esto? Tal vez simplemente no entiendo los JWT lo suficiente todavía.
Lo-Tan
77
Dos preguntas / inquietudes: 1- Caso de aplicación web: ¿por qué no se puede permitir que el token caducado se actualice? Supongamos que establecemos un vencimiento breve (1 hora) y realizamos llamadas de renovación al servidor de back-end cuando vence un token, como usted dijo. 2- ¿Existe algún problema de seguridad con el almacenamiento de la contraseña hash (con sal aleatoria) en el token? La idea es que si está allí, el servidor de fondo puede verificar la contraseña almacenada en la base de datos cuando se le solicite una renovación, y rechazar la solicitud si las contraseñas no coinciden. Esto cubriría el cambio de contraseña de la aplicación móvil / nativa, permitiendo que la solución se extienda al caso de uso móvil.
psamaan
8
-1 Exponer una API pública que vuelve a firmar ciegamente cualquier token para extender su período de validación es malo. Ahora todas tus fichas tienen una caducidad infinita efectiva. El acto de firmar un token debe incluir las verificaciones de autenticación apropiadas para cada reclamo realizado en ese token al momento de la firma.
Phil
69

En el caso de que maneje la autenticación usted mismo (es decir, no use un proveedor como Auth0), lo siguiente puede funcionar:

  1. Emita el token JWT con un vencimiento relativamente corto, digamos 15 minutos.
  2. La aplicación verifica la fecha de vencimiento del token antes de cualquier transacción que requiera un token (el token contiene la fecha de vencimiento). Si el token ha expirado, primero le pide a la API que 'actualice' el token (esto se hace de forma transparente en la UX).
  3. La API obtiene la solicitud de actualización de token, pero primero verifica la base de datos del usuario para ver si se ha establecido un indicador de 'reautorización' en ese perfil de usuario (el token puede contener una identificación de usuario). Si el indicador está presente, se rechaza la actualización del token; de lo contrario, se emite un nuevo token.
  4. Repetir.

El indicador 'reauth' en el backend de la base de datos se establecería cuando, por ejemplo, el usuario haya restablecido su contraseña. La bandera se elimina cuando el usuario inicia sesión la próxima vez.

Además, supongamos que tiene una política por la cual un usuario debe iniciar sesión al menos una vez cada 72 horas. En ese caso, la lógica de actualización del token de la API también verificará la última fecha de inicio de sesión del usuario desde la base de datos del usuario y denegará / permitirá la actualización del token sobre esa base.

IanB
fuente
77
No creo que esto sea seguro. Si yo fuera un atacante y robara tu token y lo enviara al servidor, el servidor verificaría y vería que la bandera está establecida en verdadero, lo cual es excelente ya que bloquearía una actualización. Creo que el problema sería que si la víctima cambia su contraseña, el indicador se establecerá en falso y ahora el atacante puede usar ese token original para actualizar.
user2924127
66
@ user2924127 ninguna solución de autenticación es perfecta, y siempre habrá compensaciones. Si un atacante está en posición de 'robar su ficha', entonces puede tener mayores problemas de los que preocuparse. Establecer una vida útil máxima del token sería un ajuste útil a lo anterior.
IanB
27
en lugar de tener otro campo en la base de datos, reautorizar bandera, puede incluir para hash (bcrypt_password_hash) en el token. Luego, al actualizar el token, solo confirma si hash (bcrypt_password_hash) es igual a un valor del token. Para denegar la actualización del token, uno solo tiene que actualizar el hash de contraseña.
bas
44
@bas, pensando en las optimizaciones y el rendimiento, creo que la validación de hash de contraseña sería redundante y tendría más implicaciones para el servidor. Aumente el tamaño del token para que la firma / validación de firma lleve más tiempo. cálculos hash adicionales para el servidor para la contraseña. con el enfoque de campo adicional, solo se valida en el recálculo con un booleano simple. Las actualizaciones de Db son menos frecuentes para el campo adicional, pero son más frecuentes las actualizaciones de tokens. Y obtiene el servicio opcional de forzar reinicios de sesión individuales para cualquier sesión existente (móvil, web, etc.).
le0diaz
66
Creo que el primer comentario del usuario2924127 es realmente incorrecto. Cuando se cambia la contraseña, la cuenta se marca como que requiere una nueva autenticación, por lo que los tokens caducados existentes no serán válidos.
Ralph
15

Estaba jugando cuando movía nuestras aplicaciones a HTML5 con API RESTful en el back-end. La solución que se me ocurrió fue:

  1. El cliente recibe un token con un tiempo de sesión de 30 minutos (o el tiempo de sesión habitual del lado del servidor) después de iniciar sesión correctamente.
  2. Se crea un temporizador del lado del cliente para llamar a un servicio y renovar el token antes de que expire. El nuevo token reemplazará el existente en futuras llamadas.

Como puede ver, esto reduce las frecuentes solicitudes de token de actualización. Si el usuario cierra el navegador / aplicación antes de que se active la llamada de renovación de token, el token anterior caducará a tiempo y el usuario tendrá que volver a iniciar sesión.

Se puede implementar una estrategia más complicada para atender la inactividad del usuario (por ejemplo, descuidó una pestaña de navegador abierta). En ese caso, la llamada de token de renovación debe incluir el tiempo de vencimiento esperado que no debe exceder el tiempo de sesión definido. La aplicación deberá realizar un seguimiento de la última interacción del usuario en consecuencia.

No me gusta la idea de establecer una caducidad larga, por lo tanto, este enfoque puede no funcionar bien con aplicaciones nativas que requieren autenticación menos frecuente.

Coolersport
fuente
1
¿Qué pasa si la computadora fue suspendida / suspendida? El temporizador seguirá contando hasta el vencimiento, pero el token ya había expirado. El temporizador no funciona en estas situaciones
Alex Parij
@AlexParij Se compararía con un tiempo fijo, algo como esto: stackoverflow.com/a/35182296/1038456
Aparajita
2
Permitir que el cliente solicite un nuevo token con una fecha de vencimiento preferida me parece un riesgo de seguridad.
java-addict301
14

Una solución alternativa para invalidar JWT, sin ningún almacenamiento seguro adicional en el backend, es implementar una nueva jwt_versioncolumna entera en la tabla de usuarios. Si el usuario desea cerrar sesión o caducar los tokens existentes, simplemente incrementan el jwt_versioncampo.

Al generar un nuevo JWT, codifíquelo jwt_versionen la carga útil de JWT, incrementando opcionalmente el valor de antemano si el nuevo JWT debe reemplazar a todos los demás.

Al validar el JWT, el jwt_versioncampo se compara junto con el user_idy la autorización se otorga solo si coincide.

Ollie Bennett
fuente
1
Esto tiene problemas con múltiples dispositivos. Esencialmente, si cierra sesión en un dispositivo, se cierra en todas partes. ¿Derecha?
Sam Washburn
44
Oye, eso puede no ser un "problema" dependiendo de tus requisitos, pero tienes razón; Esto no es compatible con la gestión de sesión por dispositivo.
Ollie Bennett
¿No significa esto que jwt_version debe almacenarse en el lado del servidor de modo que el esquema de autenticación se vuelva "similar a una sesión" y anule el propósito fundamental de los JWT?
ChetPrickles
8

Buena pregunta, y hay una gran cantidad de información en la pregunta misma.

El artículo Actualizar tokens: cuándo usarlos y cómo interactúan con JWT da una buena idea para este escenario. Algunos puntos son: -

  • Los tokens de actualización llevan la información necesaria para obtener un nuevo token de acceso.
  • Los tokens de actualización también pueden caducar, pero son bastante duraderos.
  • Los tokens de actualización generalmente están sujetos a estrictos requisitos de almacenamiento para garantizar que no se filtren.
  • También pueden ser incluidos en la lista negra por el servidor de autorización.

También eche un vistazo a auth0 / angular-jwt angularjs

Para API web. lea Habilitar tokens de actualización de OAuth en la aplicación AngularJS utilizando ASP .NET Web API 2 y Owin

LCJ
fuente
Tal vez lo leí mal ... Pero el artículo con un título que comienza como "Actualizar tokens ..." no contiene nada sobre tokens de actualización, excepto lo que mencionaste aquí.
Ievgen Martynov
8

De hecho, implementé esto en PHP usando el cliente Guzzle para hacer una biblioteca de cliente para la API, pero el concepto debería funcionar para otras plataformas.

Básicamente, emito dos tokens, uno corto (5 minutos) y uno largo que expira después de una semana. La biblioteca del cliente usa middleware para intentar una actualización del token corto si recibe una respuesta 401 a alguna solicitud. Luego volverá a intentar la solicitud original y, si pudo actualizar, obtiene la respuesta correcta, de forma transparente para el usuario. Si falla, solo enviará el 401 al usuario.

Si el token corto ha caducado, pero sigue siendo auténtico y el token largo es válido y auténtico, actualizará el token corto utilizando un punto final especial en el servicio que el token largo autentica (esto es lo único para lo que se puede usar). Luego usará el token corto para obtener un nuevo token largo, extendiéndolo así una semana más cada vez que actualice el token corto.

Este enfoque también nos permite revocar el acceso en un máximo de 5 minutos, lo cual es aceptable para nuestro uso sin tener que almacenar una lista negra de tokens.

Edición tardía: volver a leer este mes después de que estaba fresco en mi cabeza, debo señalar que puede revocar el acceso al actualizar el token corto porque brinda la oportunidad de llamadas más caras (por ejemplo, llame a la base de datos para ver si el usuario ha sido prohibido) sin pagar por cada llamada a su servicio.

BytePorter
fuente
8

A continuación se detallan los pasos para revocar su token de acceso JWT:

1) Cuando inicie sesión, envíe 2 tokens (token de acceso, token de actualización) en respuesta al cliente.
2) El token de acceso tendrá menos tiempo de vencimiento y Actualizar tendrá un tiempo de vencimiento largo.
3) El Cliente (Front end) almacenará el token de actualización en su almacenamiento local y el token de acceso en las cookies.
4) El cliente usará token de acceso para llamar a apis. Pero cuando caduque, elija el token de actualización del almacenamiento local y llame a la API del servidor de autenticación para obtener el nuevo token.
5) Su servidor de autenticación tendrá una API expuesta que aceptará el token de actualización y verificará su validez y devolverá un nuevo token de acceso.
6) Una vez que el token de actualización ha caducado, el usuario cerrará sesión.

Avíseme si necesita más detalles, también puedo compartir el código (arranque Java + Spring).

Bhupinder Singh
fuente
¿Podría compartir el enlace de su proyecto si lo tiene en GitHub?
Arun Kumar N
6

jwt-autorefresh

Si está utilizando el nodo (React / Redux / Universal JS) puede instalarlo npm i -S jwt-autorefresh.

Esta biblioteca programa la actualización de tokens JWT a un número calculado por el usuario de segundos antes de que expire el token de acceso (según el reclamo de exp codificado en el token). Tiene un extenso conjunto de pruebas y verifica algunas condiciones para garantizar que cualquier actividad extraña vaya acompañada de un mensaje descriptivo sobre las configuraciones incorrectas de su entorno.

Implementación de ejemplo completo

import autorefresh from 'jwt-autorefresh'

/** Events in your app that are triggered when your user becomes authorized or deauthorized. */
import { onAuthorize, onDeauthorize } from './events'

/** Your refresh token mechanism, returning a promise that resolves to the new access tokenFunction (library does not care about your method of persisting tokens) */
const refresh = () => {
  const init =  { method: 'POST'
                , headers: { 'Content-Type': `application/x-www-form-urlencoded` }
                , body: `refresh_token=${localStorage.refresh_token}&grant_type=refresh_token`
                }
  return fetch('/oauth/token', init)
    .then(res => res.json())
    .then(({ token_type, access_token, expires_in, refresh_token }) => {
      localStorage.access_token = access_token
      localStorage.refresh_token = refresh_token
      return access_token
    })
}

/** You supply a leadSeconds number or function that generates a number of seconds that the refresh should occur prior to the access token expiring */
const leadSeconds = () => {
  /** Generate random additional seconds (up to 30 in this case) to append to the lead time to ensure multiple clients dont schedule simultaneous refresh */
  const jitter = Math.floor(Math.random() * 30)

  /** Schedule autorefresh to occur 60 to 90 seconds prior to token expiration */
  return 60 + jitter
}

let start = autorefresh({ refresh, leadSeconds })
let cancel = () => {}
onAuthorize(access_token => {
  cancel()
  cancel = start(access_token)
})

onDeauthorize(() => cancel())

descargo de responsabilidad: soy el mantenedor

cchamberlain
fuente
Pregunta sobre esto, vi la función de decodificación que usa. ¿Asume que el JWT se puede decodificar sin usar un secreto? ¿Funciona con JWT que se firmaron con un secreto?
Gian Franco Zabarino
3
Sí, la decodificación es una decodificación solo para clientes y no debe conocer el secreto. El secreto se usa para firmar el token JWT del lado del servidor para verificar que su firma se usó para generar el JWT originalmente y nunca se debe usar desde el cliente. La magia de JWT es que su carga útil puede decodificarse en el lado del cliente y las reclamaciones internas pueden usarse para construir su IU sin el secreto. Lo único que lo jwt-autorefreshdescodifica es extraer el expreclamo para que pueda determinar qué tan lejos programar la próxima actualización.
cchamberlain
1
Es bueno saber que algo no tenía sentido, pero ahora sí. Gracias por la respuesta.
Gian Franco Zabarino
4

Resolví este problema agregando una variable en los datos del token:

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

Establezco la expiresInopción en el tiempo deseado antes de que el usuario se vea obligado a iniciar sesión nuevamente. La mía se establece en 30 minutos. Esto debe ser mayor que el valor de softexp.

Cuando mi aplicación del lado del cliente envía una solicitud a la API del servidor (donde se requiere token, por ejemplo, página de lista de clientes), el servidor verifica si el token enviado sigue siendo válido o no en función de su expiresInvalor de vencimiento ( ) original . Si no es válido, el servidor responderá con un estado particular para este error, por ejemplo. INVALID_TOKEN.

Si el token sigue siendo válido en función del expiredInvalor, pero ya excedió el softexpvalor, el servidor responderá con un estado separado para este error, por ejemplo. EXPIRED_TOKEN:

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

En el lado del cliente, si recibió EXPIRED_TOKEN respuesta, debería renovar el token automáticamente enviando una solicitud de renovación al servidor. Esto es transparente para el usuario y se ocupa automáticamente de la aplicación cliente.

El método de renovación en el servidor debe verificar si el token sigue siendo válido:

jwt.verify(token, secret, (err, decoded) => {})

El servidor se negará a renovar tokens si falló el método anterior.

James A
fuente
Esta estrategia se ve bien. Pero creo que debería complementarse con una especie de "cantidad máxima de renovaciones" porque (tal vez) una sesión de usuario puede estar viva para siempre.
Juan Ignacio Barisich
1
Puede establecer una variable hardExp en los datos del token para establecer una fecha máxima para forzar el vencimiento del token, o tal vez un contador que disminuye cada vez que se renueva el token, lo que limita la cantidad de renovaciones totales del token.
James A
1
eso es correcto. Considero que esto es "imprescindible".
Juan Ignacio Barisich
2

¿Qué tal este enfoque:

  • Para cada solicitud de cliente, el servidor compara el tiempo de expiración del token con (currentTime - lastAccessTime)
  • Si expirationTime <(currentTime - lastAccessedTime) , cambia el último lastAccessedTime a currentTime.
  • En caso de inactividad en el navegador por un período de tiempo superior a expirationTime o en caso de que se cerró la ventana del navegador y expirationTime> (currentTime - lastAccessedTime) , el servidor puede expirar el token y pedirle al usuario que vuelva a iniciar sesión.

No requerimos un punto final adicional para actualizar el token en este caso. Agradecería cualquier feedack.

sjaiswal
fuente
¿Es una buena opción en este día? Parece bastante fácil de implementar.
b.ben
44
En este caso, ¿dónde almacena lastAccessedTime? Debe hacerlo en el backend y por solicitud, por lo que se convierte en una solución con estado no deseada.
antgar9
2

Hoy en día, muchas personas optan por administrar las sesiones con JWT sin darse cuenta de lo que están renunciando por la simpleza percibida . Mi respuesta se desarrolla en la segunda parte de las preguntas:

¿Cuál es el beneficio real entonces? ¿Por qué no tener solo un token (no JWT) y mantener la caducidad en el servidor?

¿Hay otras opciones? ¿Usar JWT no es adecuado para este escenario?

Los JWT son capaces de admitir la gestión de sesión básica con algunas limitaciones. Al ser tokens autodescriptivos, no requieren ningún estado en el lado del servidor. Esto los hace atractivos. Por ejemplo, si el servicio no tiene una capa de persistencia, no necesita traer una solo para la administración de la sesión.

Sin embargo, la apatridia también es la causa principal de sus deficiencias. Dado que solo se emiten una vez con contenido fijo y caducidad, no puede hacer las cosas que le gustaría con una configuración de administración de sesión típica.

Es decir, no puede invalidarlos a pedido. Esto significa que no puede implementar un cierre de sesión seguro ya que no hay forma de expirar los tokens ya emitidos. Tampoco puede implementar el tiempo de espera inactivo por el mismo motivo. Una solución es mantener una lista negra, pero eso introduce el estado.

Escribí una publicación explicando estos inconvenientes con más detalle. Para que quede claro, puede solucionarlos agregando más complejidad (sesiones deslizantes, tokens de actualización, etc.)

En cuanto a otras opciones, si sus clientes solo interactúan con su servicio a través de un navegador, le recomiendo utilizar una solución de administración de sesión basada en cookies. También compilé una lista de métodos de autenticación actualmente ampliamente utilizados en la web.

Daniel Szpisjak
fuente