Prolongación automática del vencimiento de JWT (JSON Web Token)
Me gustaría implementar la autenticación basada en JWT en nuestra nueva API REST. Pero dado que el vencimiento está establecido en el token, ¿es posible prolongarlo automáticamente? No quiero que los usuarios tengan que iniciar sesión cada X minutos si estuvieron usando activamente la aplicación durante ese período. Eso sería un gran fracaso de UX.
Pero prolongar el vencimiento 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 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 antiguo usado usando una lista negra, pero necesitaría almacenar los tokens. Y uno de los beneficios de JWT es que no hay almacenamiento.
Descubrí cómo lo resolvió Auth0. Usan no solo el token JWT sino también un token de actualización: https://auth0.com/docs/tokens/refresh-tokens
Pero nuevamente, para implementar esto (sin Auth0), necesitaría almacenar tokens de actualización y mantener su vencimiento. ¿Cuál es entonces el beneficio real? ¿Por qué no tener un solo token (no JWT) y mantener el vencimiento en el servidor?
¿Hay otras opciones? ¿El uso de JWT no es adecuado para este escenario?
Trabajo en Auth0 y participé en el diseño de la función del token de actualización.
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 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 experiencia de usuario de aplicación web aceptable.
Para actualizar el token, su API necesita un nuevo punto final que reciba un JWT válido y no vencido y devuelva el mismo JWT firmado con el nuevo campo de vencimiento. 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 sólo una vez.
La idea es que el token de actualización nunca caduque y siempre se pueda canjear 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, el usuario debe poder identificarlo de alguna manera y la aplicación debe proporcionar una forma de revocar el acceso. Decidimos utilizar 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 consiste en revocar el token de actualización en eventos específicos. Un evento interesante es el cambio de contraseña.
Creemos que JWT no es útil para estos casos de uso, por lo que usamos una cadena generada aleatoriamente y la almacenamos en nuestro lado.
En el caso de que usted mismo maneje la autenticación (es decir, no use un proveedor como Auth0), lo siguiente puede funcionar:
- Emitir token JWT con vencimiento relativamente corto, digamos 15 minutos.
- 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 caducado, primero le pide a la API que "actualice" el token (esto se hace de forma transparente para la UX).
- La API recibe una solicitud de actualización del token, pero primero verifica la base de datos del usuario para ver si se ha establecido un indicador de 'reauth' en ese perfil de usuario (el token puede contener una identificación de usuario). Si el indicador está presente, se deniega la actualización del token; de lo contrario, se emite un nuevo token.
- 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 la próxima vez que el usuario inicie sesión.
Además, digamos 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 API también verificaría la fecha del último inicio de sesión del usuario en la base de datos del usuario y denegaría/permitiría la actualización del token sobre esa base.
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 la actualización tendrá un tiempo de vencimiento más largo.
3) El cliente (frontal) almacenará el token de actualización en su almacenamiento local y el token de acceso en las cookies.
4) El cliente utilizará un token de acceso para llamar a las API. 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, verificará su validez y devolverá un nuevo token de acceso.
6) Una vez que el token de actualización caduque, se cerrará la sesión del usuario.
Avíseme si necesita más detalles. También puedo compartir el código (Java + Spring boot).
Estaba trasteando al mover nuestras aplicaciones a HTML5 con API RESTful en el backend. La solución que se me ocurrió fue:
- El cliente recibe un token con un tiempo de sesión de 30 minutos (o el tiempo habitual de sesión del lado del servidor) al iniciar sesión correctamente.
- Se crea un temporizador del lado del cliente para llamar a un servicio y renovar el token antes de que expire. El nuevo token sustituirá al existente en próximas convocatorias.
Como puede ver, esto reduce las solicitudes frecuentes de tokens de actualización. Si el usuario cierra el navegador o la aplicación antes de que se active la llamada de renovación del 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, descuidar una pestaña abierta del navegador). En ese caso, la llamada del token de renovación debe incluir el tiempo de vencimiento esperado que no debe exceder el tiempo de sesión definido. En consecuencia, la aplicación deberá realizar un seguimiento de la última interacción del usuario.
No me gusta la idea de establecer una caducidad prolongada, por lo que es posible que este enfoque no funcione bien con aplicaciones nativas que requieren una autenticación menos frecuente.
Una solución alternativa para invalidar los JWT, sin ningún almacenamiento seguro adicional en el backend, es implementar una nueva jwt_version
columna de enteros en la tabla de usuarios. Si el usuario desea cerrar sesión o caducar los tokens existentes, simplemente incrementa el jwt_version
campo.
Al generar un nuevo JWT, codifique jwt_version
en la carga útil del JWT y, opcionalmente, incremente el valor de antemano si el nuevo JWT debe reemplazar a todos los demás.
Al validar el JWT, el jwt_version
campo se compara junto con el user_id
y se otorga autorización solo si coincide.
jwt-autorefresh
Si está utilizando nodo (React/Redux/Universal JS), puede instalar npm i -S jwt-autorefresh
.
Esta biblioteca programa la actualización de los tokens JWT en una cantidad de segundos calculada por el usuario 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 verifica bastantes condiciones para garantizar que cualquier actividad extraña vaya acompañada de un mensaje descriptivo sobre 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