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

Me gustaría implementar la autenticación basada en JWT a nuestra nueva API REST. Pero dado que el vencimiento se establece en el token, ¿es posible prolongarlo automáticamente? No deseo 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 UX.

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

Encontré cómo Auth0 lo resolvió. No solo usan 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 caducidad. ¿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?

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

Todo depende del tipo de aplicación y este es 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, deberá iniciar sesión de nuevo y esta es una UX de aplicación web aceptable.

Para actualizar el token, su API necesita un nuevo punto final que reciba un JWT válido y no caducado y 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 sola vez.

La idea es que el token de actualización nunca caduque y se pueda 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, así que usamos una cadena generada aleatoriamente y la almacenamos de nuestro lado.

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 15min.
  2. La aplicación verifica la fecha de caducidad del token antes de cualquier transacción que requiera un token (el token contiene la fecha de caducidad). Si token ha caducado, primero se le pide a la API que ‘actualice’ el token (esto se hace de forma transparente para el UX).
  3. API recibe una solicitud de actualización de token, pero primero verifica la base de datos de usuario para ver si se ha establecido un indicador ‘reauth’ contra ese perfil de usuario (el token puede contener el ID 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á cuando, por ejemplo, el usuario haya restablecido su contraseña. El indicador se elimina cuando el usuario inicia sesión la próxima vez.

Además, supongamos que tiene una política según 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ía 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.

Estaba retocando al mover nuestras aplicaciones a HTML5 con apis 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) al iniciar sesión correctamente.
  2. Un temporizador del lado del cliente se crea para llamar a un servicio para renovar el token antes de que expire el tiempo. El nuevo token reemplazará las llamadas existentes en futuras.

Como puede ver, esto reduce las frecuentes solicitudes de actualización de token. Si el usuario cierra el navegador / aplicación antes de que se active la llamada de token de renovación, 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, descuidar una pestaña del navegador abierta). En ese caso, la llamada de token de renovación debe incluir el tiempo de caducidad esperado que no debe exceder el tiempo de sesión definido. La aplicación tendrá que realizar un seguimiento de la última interacción del usuario en consecuencia.

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

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

Al generar un nuevo JWT, codifique el jwt_version en la carga JWT, incrementando opcionalmente el valor de antemano si el nuevo JWT debe reemplazar a todos los demás.

Al validar el JWT, el campo jwt_version se compara junto con el user_id y la autorización se concede solo si coincide.

Buena pregunta, y hay mucha información en la pregunta en sí.

El artículo Refresh 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 contienen 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 incluidas en la lista negra por el servidor de autorizaciones.

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

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

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

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

Si el token corto está vencido, 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 autentica el token largo (esto es lo único que se puede usar). Luego usará el token corto para obtener un nuevo token largo, extendiéndolo una semana más cada vez que actualice el token corto.

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

Edición tardía: volviendo a leer estos meses 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 realizar llamadas más costosas (por ejemplo, llamar a la base de datos para ver si el usuario ha sido prohibido) sin pagar en cada llamada a su servicio.

jwt-autorefresh

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

Esta biblioteca progtwig la actualización de los tokens JWT en un número calculado por el usuario de segundos antes de que caduque el token de acceso (según el reclamo de exp codificado en el token). Tiene un amplio conjunto de pruebas y comprueba 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 completa de ejemplos

 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: yo soy el mantenedor

¿Qué tal este enfoque?

  • Para cada solicitud del cliente, el servidor compara el expirationTime del token con (currentTime – lastAccessTime)
  • Si expirationTime <(currentTime - lastAccessedTime) , cambia el último lastAccessedTime to currentTime.
  • En caso de inactividad en el navegador por un tiempo que exceda el tiempo de expiración o en caso de que la ventana del navegador esté cerrada y expirationTime> (currentTime – lastAccessedTime) , y luego el servidor puede caducar el token y pedirle al usuario que vuelva a iniciar sesión.

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

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

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

Establecí la opción expiresIn en mi tiempo deseado antes de que el usuario se vea obligado a iniciar sesión de nuevo. El mío está configurado 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 el token, por ejemplo, la página de la lista de clientes), el servidor comprueba si el token enviado sigue siendo válido o no en su valor original de expiración ( expiresIn ). Si no es válido, el servidor responderá con un estado particular para este error, por ej. INVALID_TOKEN .

Si el token sigue siendo válido en expiredIn valor de expiredIn , pero ya excedió el valor de softexp , 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ó la respuesta EXPIRED_TOKEN , debe 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 del 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 rechazará renovar tokens si falla el método anterior.