Cuando se diseña una aplicación web, la autenticación es una de las piezas claves, ya que la seguridad depende en gran medida de este punto. La autenticación con tokens supuso un gran avance en este aspecto, y el refresh token llegó para complementarla y hacerla usable.
Autenticación
Los sistemas de autenticación se dividen según el modo en que verifican al usuario:
– Basados en algo conocido (password)
– Basados en algo poseído (tarjeta de identidad, usb, token)
– Basados en características físicas (voz, huellas, ojos)
Autenticación basada en token
Los tokens fueron introducidos en las aplicaciones web por la autenticación y autorización moderna. Podríamos decir que su uso se extendió gracias al protocolo OAuth (posteriormente OAuth2). Estos estaban centrados en la autorización, y no en la autenticación como se tiende a pensar.
Cuando hablamos de autenticación con tokens, podemos dividirlo en 2 tipos:
1. Autenticación tradicional o autenticación en servidor
Hasta hace poco ha sido el modo de autenticación más habitual. Cuando un usuario se loguea, el servidor le devuelve un token que típicamente es almacenado en una cookie. El servidor guarda la información de la sesión, bien en memoria o en base de datos (Redis, MongoDB …).
De este modo, cada vez que el usuario hace una petición con ese token, el servidor busca la información para saber qué usuario está intentando acceder y si es válida, ejecuta el método solicitado.
Este tipo de autenticación tiene varios problemas, como la sobrecarga provocada por toda la información de los usuarios autenticados. Así como de escalabilidad, ya que si hay varias instancias del servidor levantadas, tendrían que compartir de algún modo la información de la sesión para no hacerle logarse de nuevo.
Además, existen vulnerabilidades debidas a esta arquitectura (CORS, CSRF).
2. Autenticación sin estado basada en tokens
Para solucionar todos estos inconvenientes, surge la autenticación sin estado (stateless). Esto significa que el servidor no va a almacenar ninguna información, ni tampoco la sesión.
Cuando el usuario se autentica con sus credenciales o cualquier otro método, en la respuesta recibe un token (access token). A partir de ese momento, todas las peticiones que se hagan al API llevarán este token en una cabecera HTTP de modo que el servidor pueda identificar qué usuario hace la petición sin necesidad de buscar en base de datos ni en ningún otro sistema de almacenamiento.
Con este enfoque, la aplicación pasa a ser escalable, ya que es el propio cliente el que almacena su información de autenticación, y no el servidor. Así las peticiones pueden llegar a cualquier instancia del servidor y podrá ser atendida sin necesidad de sincronizaciones.
Diferentes plataformas podrán usar el mismo API
Además se incrementa la seguridad, evitando vulnerabilidades CSRF, al no existir sesiones. Y si añadimos expiración al token la seguridad será aún mayor.
JWT (JSON Web Token)
JSON Web Token (JWT) es un estandar abierto basado en JSON para crear tokens de acceso que permiten el uso de recursos de una aplicación o API. Este token llevará incorporada la información del usuario que necesita el servidor para identificarlo, así como información adicional que pueda serle útil (roles, permisos, etc.).
Además podrá llevar incorporado tiempo de validez. Una vez pasado este tiempo de validez, el servidor no permitirá más el acceso a recursos con dicho token. En este paso, el usuario tendrá que conseguir un nuevo access token volviéndose a autenticar o con algún método adicional: refresh token.
JWT define JSON como el formato interno a usar por la información almacenada en el token. Además, puede llegar a ser muy útil si se usa junto a JSON Web Signature (JWS) y JSON Web Encryption (JWE).
La combinación de JWT junto con JWS y JWE nos permite no sólo autenticar al usuario, sino enviar la información encriptada para que sólo el servidor pueda extraerla, así como validar el contenido y asegurarse que no ha habido suplantaciones o modificaciones.
Un token JWT está formado por 3 partes separadas por un . siendo cada una de ellas:
- cabecera (header): con el tipo (JWT) y el tipo de codificación
{ "alg": "HS256", "typ": "JWT" }
- Cuerpo (payload): Es donde se encontrará la información del usuario que permitirá al servidor discernir si puede o no acceder al recurso solicitado
{ username: 'john_doe', email: 'john_doe@server.com', name: 'John Doe', role: 'user', exp: 1478773621 }
- Firma de verificación (signature): Se aplicará la función de firmado a los otros dos campos del token para obtener el campo de verificación
Tipos de token
Hay muchos tipos de token, aunque en la autenticación con JWT los más típicos son el access token y el refresh token.
- Access token: Lleva contenida toda la información que necesita el servidor para saber si el usuario / dispositivo puede acceder al recurso que está solicitando o no. Suelen ser tokens caducos con un periodo de validez corto.
- Refresh token: El refresh token es usado para generar un nuevo access token. Típicamente, si el access token tiene fecha de expiración, una vez que caduca, el usuario tendría que autenticarse de nuevo para obtener un access token. Con el refresh token, este paso se puede saltar y con una petición al API obtener un nuevo access token que permita al usuario seguir accediendo a los recursos de la aplicación.
También puede ser necesario generar un nuevo access token cuando se quiere acceder a un recurso que no se ha accedido con anterioridad, aunque esto depende de las restricciones en la implementación del API.
El refresh token requiere una seguridad mayor a la hora de ser almacenado que el access token, ya que si fuera sustraido por terceras partes, podrían utilizarlo para obtener access tokens y acceder a los recursos protegidos de la aplicación. Para poder cortar un escenario como este, debe implementarse en el servidor algún sistema que permita invalidar un refresh token, además de establecer un tiempo de vida que obviamente debe ser más largo que el de los access tokens.
Refresh token y JWT. Implementación en Node.js
Para este ejemplo voy a saltarme la parte de base de datos y por tanto algunas comprobaciones de seguridad que deberían hacerse, aunque las iré comentando. El motivo es mostrar un código lo más sencillo posible y no condicionar la implementación a ningún sistema de permanencia.
En este primer código simplemente arrancamos un servidor node como haríamos con cualquier otra aplicación.
var express = require('express')
var app = express()
app.listen(8999)
Lo primero que vamos a añadir es un método para que el usuario se autentique. El método de autenticación puede ser cualquiera, aunque el más típico es usar username y password. Este es el que hemos usado, aunque para simplificar el código no se comprueba contra base de datos y permitimos el acceso a todos los usuarios (con cualquier password).
En la respuesta retornaremos tanto el token JWT como el refresh token con el que podrá solicitar nuevos tokens de acceso. Como vemos en la implementación el token se está creando con un tiempo de validez de 300 segundos (5 minutos).
Con el módulo jsonwebtoken encriptaremos y generaremos la firma, es decir, automáticamente nos generará el token JWT simplemente pasándole el objeto a encriptar y la clave que usaremos tanto para encriptar como para desencriptar después.
Para el refresh token, simplemente generaremos un UID y lo almacenaremos en un objeto en memoria junto con el username del usuario asociado. Lo normal sería guardarlo en una base de datos con la información del usuario y la fecha de creación y de expiración (si es que queremos que tenga un tiempo limitado de validez).
También se podría hacer que fuera autocontenido, como los access tokens que creamos. La ventaja que daría esta implementación es no acceder a base de datos para sacar la información necesaria. Pero en este caso no nos permitiría saber si el refresh token ha sido puesto en la lista negra o anulado por un administrador, con lo que no nos interesa. O si se ha desabilitado al usuario por algún administrador tampoco nos daríamos cuenta. Por eso este tipo de tokens prefiero implementarlo sin información autocontenida.
var bodyParser = require('body-parser')
var jwt = require('jsonwebtoken')
var randtoken = require('rand-token')
var refreshTokens = {}
var SECRET = "SECRETO_PARA_ENCRIPTACION"
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.post('/login', function (req, res, next) {
var username = req.body.username
var password = req.body.password
var user = {
'username': username,
'role': 'admin'
}
var token = jwt.sign(user, SECRET, { expiresIn: 300 })
var refreshToken = randtoken.uid(256)
refreshTokens[refreshToken] = username res.json({token: 'JWT ' + token, refreshToken: refreshToken})
});
Para solicitar un nuevo access token hemos creado el recurso /token. En él recibimos el refresh token y como control adicional el username del usuario que es dueño del refresh token. Aquí lo que haremos será comprobar que en nuestra lista de refresh tokens está el que nos envían y que tiene el mismo username asociado. Si es correcto, generamos un nuevo token con la información del usuario (que obtendríamos de la base de datos) y lo devolvemos.
Si en nuestra aplicación el administrador pudiera deshabilitar usuarios o refresh tokens temporalmente, tendríamos que comprobarlo también antes de generar el nuevo access token.
app.post('/token', function (req, res, next) {
var username = req.body.username
var refreshToken = req.body.refreshToken
if((refreshToken in refreshTokens) && (refreshTokens[refreshToken] == username)) {
var user = {
'username': username,
'role': 'admin'
}
var token = jwt.sign(user, SECRET, { expiresIn: 300 })
res.json({token: 'JWT ' + token})
}
else {
res.send(401)
}
})
En esta arquitectura es necesario tener un modo de deshabilitar un refresh token, para los casos en los que pueda ser sustraído, y así evitar suplantaciones y mal uso.
En una aplicación en la que un usuario puede estar trabajando desde diferentes dispositivos, con una sola identidad (mismo username) pero con tokens diferentes en cada dispositivo, si pierde o le roban uno de estos, este método le permitiría al administrador borrar o deshabilitar el refresh token en cuestión sin necesidad de que el usuario se quede sin servicio en el resto de dispositivos. Ni que tenga que volver a autenticarse, ni cambiar su password, etc. Es decir, podría seguir trabajando sin que le influya en nada y sin riesgo de que puedan generarle nuevos access tokens desde el dispositivo sustraído. Es recomendable que los access tokens tengan un tiempo de vida corto para que en casos como este, se pueda volver a un estado seguro rápidamente.
Para ello hemos creado un recurso /token/reject por el que se puede deshabilitar un refresh token. En este caso simplemente lo borramos de nuestra lista en memoria. En una implementación completa habría que comprobar que el usuario que hace la petición es administrador o tiene los permisos para este recurso.
app.post('/token/reject', function (req, res, next) {
var refreshToken = req.body.refreshToken
if(refreshToken in refreshTokens) {
delete refreshTokens[refreshToken]
}
res.send(204)
})
Por último, vamos a exponer un recurso al que sólo se podrá acceder enviando una cabecera con un token JWT conseguido con anterioridad, y que habrá sido generado por nuestra aplicación y firmado con nuestra clave (SECRET)
En este caso vamos a hacer uso de Passport. Passport es un middleware para autenticación en Node.js. Es muy flexible y modular. Esto se plasma en una gran cantidad de módulos, cada uno de los cuales implementa una estrategia de autenticación diferente (JWT, Twitter, Facebook, Google, Auth0, SAML … y así hasta más de 300). Podemos usar cualquiera de ellas, importándola y configurándola de manera muy sencilla y delegando la parte más compleja de la autenticación en Passport.
En primer lugar cargaremos el middleware y los objetos necesarios. Passport require que implementemos el métodos serializeUser (y dependiendo de la estrategia también el deserializeUser), que sirven para que el middleware almacene el objeto usuario en la sesión con los campos que queramos y le digamos por qué campo queremos que lo indexe. En nuestro ejemplo lo indexamos por el username, pero lo ideal sería usar un ID.
Lo cierto es que al ser autenticación sin estado las sesiones no tienen sentido, y es que Passport nunca llegará a usar la deserialización si sólo usamos JWT. La he dejado comentada por si quisiéramos introducir nuevas estrategias.
var passport = require('passport')
var JwtStrategy = require('passport-jwt').Strategy
var ExtractJwt = require('passport-jwt').ExtractJwt
app.use(passport.initialize())
app.use(passport.session())
passport.serializeUser(function (user, done) {
done(null, user.username)
})
/*
passport.deserializeUser(function (username, done) {
done(null, username)
})
*/
Para la configuración del módulo JWT sólo tenemos que decirle donde nos va a llegar el token en las peticiones, en nuestro caso lo esperamos en la cabecera Authorization, y cual es la clave para desencriptar los tokens JWT.
Y por último diremos qué queremos hacer con la información extraída del token cada vez que llega una petición a un recurso que usa esta autenticación. La variable jwtPayload tendrá el objeto usuario que encriptamos en el login del usuario:
var token = jwt.sign(user, SECRET, { expiresIn: 300 })
La configuración de nuestra estrategia quedaría como sigue:
var opts = {}
// Setup JWT options
opts.jwtFromRequest = ExtractJwt.fromAuthHeader()
opts.secretOrKey = SECRET
passport.use(new JwtStrategy(opts, function (jwtPayload, done) {
//If the token has expiration, raise unauthorized
var expirationDate = new Date(jwtPayload.exp * 1000)
if(expirationDate < new Date()) {
return done(null, false);
}
var user = jwtPayload
done(null, user)
}))
El recurso que vamos a crear para probar la autenticación es /test_jwt. Y simplemente le diremos a Passport que el acceso a ese path nos lo autentica con la estrategia «jwt». Esto nos da una idea de que con Passport podemos autenticar cada recurso con una estrategia diferente, lo cual nos da una gran flexibilidad y de una manera muy sencilla.
app.get('/test_jwt', passport.authenticate('jwt'), function (req, res) {
res.json({success: 'You are authenticated with JWT!', user: req.user})
})
Conclusión
El uso de JWT nos permite aumentar la eficiencia de nuestras aplicaciones evitando múltiples llamadas a la base de datos, y de este modo reducir la latencia. Además, con el uso de refresh tokens mejoramos la seguridad y usabilidad de esta arquitectura.
El uso de tokens para la autenticación sirve en gran número de proyectos, pero no es el Santo Grial que soluciona todos los problemas ni sirve para todos los productos, pero sí que debemos tenerla muy en cuenta al plantear cualquier solución.
Como integrar MongoDB en NodeJS
Artículos relacionados
Logging en Express.js: Identificador único para cada request – Node.js
Encriptación de password en NodeJS y MongoDB: bcrypt
OAuth2, protocolo de autorización
Mocha: Unit tests en Javascript
Controla el estilo del código de tu equipo: ESLint para JavaScript
@David its a great post. But I had doubt. When we are using passport-jwt strategy, how can I implement refresh token. Whether I need to create a token and pass it with access token when login. And also when to validate it, That is when did client knows that its token has expired. Is it required to validate refresh token in every request. Please let me know. Cheers
Hi Anuj,
thanks for reading. Many questions here, I’ll try to answer you.
The refresh token is returned with the access token when user is logged in. Client will access to the API with the access token until some request is responded with 401 Unauthorized from the server. That’s how the client knows the token is expired. At this moment Client has to request a new access token, and it has to do it with the refresh token. Client has to send the refresh token only when it requests a new access token, not in the rest of the requests of the API. And Server has to validate refresh token only when Client request a new access token.
I hope it clarifies you, other case let me know
Hi David. Thankyou for writing such a detailed post. I have a question : For refreshing the token – you require the Client to submit the username and the refresh token – Now assume this Client is Android Device which can be easily rooted so if a person finds the refresh token and knows the username –> they can keep on generating new access tokens. How do we solve this issue.
Hi Jiten,
I don’t even send the username when refreshing token. The only part is supposed to know the refresh token is the client.
As you said, in order to avoid stolen refresh tokens, we have 2 possibilities (from my point of view):
– Admin has the option to cancel manually a refresh token. If any client tell the admin something happen with its device, admin can manually cancel the refresh token, and the client will be able to authenticate again with its credentials and get a new and valid Access token. We usually take this option.
– The other option is to create refresh tokens with expiration date. You are not avoiding completly that a refresh token is stolen, but at least you are limiting the damage or the time that can be used wrongly.
Obviously both options are compatible. Depends on the project and the kind of data you are sharing.
Hello, can I find the article in english? And, do you have working example with refresh tokens on github or bitbucket repository?
Thank you
I’m afraid I didn’t have time to translate the article. Maybe in the future.
And right now We don’t have any example publishes. But main parts of the code needed are in the article. You just have to put all the pieces together
Google translate did the job just fine in my case.
Nice article! Gracias hombre.
For those looking for an english version, check the link below «google translate».
https://translate.google.com/translate?hl=en&sl=es&tl=en&u=http%3A%2F%2Fahorasomos.izertis.com%2Fsolidgear%2Frefresh-token-autenticacion-jwt-implementacion-nodejs%3Flang%3Des
Dear @Rodrigo, thank you for providing the translation link
Hola, excelente artículo. Muchas gracias. Quería preguntarte, ¿por qué dices que usar refresh token como token autocontenidas, no permite saber si esta ha sido puesta en la lista negra o anulada por un administrador?