En las entradas anteriores de la serie se ha creado una API REST que puede ser empleada por cualquiera que sepa cómo realizar las consultas. Aunque este no será el caso más habitual. En general solamente deberían poder acceder a esta los usuarios autorizados, lo que se garantiza mediante el empleo de usuarios y contraseñas. Enviar el par de usuario y contraseña en cada una de las llamadas no es seguro, ya que aumenta las posibilidades de que estas sean interceptadas. Para evitar esto una posible solución es enviar el usuario y la contraseña para iniciar sesión y posteriormente usar un token temporal. Algo que se puede conseguir mediante el uso de JWT. Veamos en esta ocasión como implementar la autenticación mediante JWT.
En esta ocasión se va a ver únicamente como usar los JWT para restringir el acceso a los servicios. La autenticación de usuarios se verá en la próxima publicación.
Qué es JWT (JSON Web Token)
JWT es un formato de token de seguridad. Básicamente es un objeto JSON codificado en base64, para facilitar su transmisión a través de la red, que contiene tres componentes: cabecera, cuerpo (o payload) y firma. En la cabecera se indica el tipo de token y el algoritmo empleado. La segunda parte, el cuerpo, se pueden incluir todos los datos que sean necesarios para identificar a un usuario, generalmente suele ser el id y otros datos públicos. Finalmente, en la parte final se almacena una firma que el creador del token ha generado con su clave secreta, para validar la originalidad del token.
En este punto es importante notar que el contenido del JWT no se encuentra encriptado, por lo que cualquiera que pueda acceder al mismo podrá leer la información del cuerpo. Aunque, si no conoce la clave de firmado, no podrá saber si es original o no. Por eso los datos que se envían en JWT deben ser confidenciales.
Ejemplo de JWT
Para ver en un ejemplo el funcionamiento de los JWT se puede acceder a la página del estándar jwt.io. En esta se puede pegar cualquier JWT en la parte derecha y ver las tres partes de este a la izquierda. Además de separarlas por colores, rojo la cabecera, magenta el cuerpo y azul a la firma.
En esta página se puede comprobar que si se cambia en cualquier carácter del token este dejara de ser válido. También se puede ver que si se modifica la clave de firmado esta dejará de ser el token tampoco será validado. Es decir, solo un token sin modificar podrá ser validado con la clave que lo ha generado. Por lo que, al recibir un token se podrá saber si este se ha generado con la palabra nuestra palabra clave y no ha sido modificado.
Además de esto también se le puede añadir una fecha de caducidad. Así, una vez pasado esta fecha, el token dejará de ser válido y será necesario que el usuario se vuelva a identificar para generar un nuevo token. Lo que supone un compromiso entre duración del token y evitar que el usuario tenga que iniciar sesión continuamente.
Instalación de JWT
Antes de poder usar en nuestro proyecto los JWT es necesario instalar los paquetes necesarios. En Node existen diferentes implementaciones de JWT pero una de las más populares es jsonwebtoken
, la cual se puede instalar escribiendo en la terminal:
npm install jsonwebtoken
Además, es necesario instalar las definiciones para TypeScript, lo que se puede hacer ejecutando la siguiente línea en la terminal
npm install --save-dev @types/jsonwebtoken
Ruta para realizar el login en la API
Ahora una vez instalado jsonwebtoken
en nuestro proyecto hay que crear una ruta para la obtención de un token. Ruta que se puede crear en /auth/login
, para lo que se creará al siguiente archivo auth.ts
en la carpeta routes
.
import { Router, Request, Response } from 'express'; import { sign } from 'jsonwebtoken'; const router = Router(); const secret = String(process.env.TOKEN_SECRET); const expiresIn = process.env.EXPIRES ? String(process.env.EXPIRES) : '15m'; router.post('/login', (_req: Request, res: Response) => { sign({ user: 'anonymous', admin: false }, secret, { expiresIn }, (err, token) => { if (err) { return res.status(500).send(`It was not possible to generate the token`); } return res.send({ token }); }); }); export default router;
En este archivo, una vez importadas las dependencias y creado el objeto Route
lo primero que se hace es importar los parámetros TOKEN_SECRET
, en el cual se almacena la palabra secreta, y EXPIRES
, en el cual se almacena el tiempo de vida del token, del entorno. Como se ha visto en una entrada anterior, ambos parámetros se deben configurar en el archivo .env
. El valor de TOKEN_SECRET
debe mantenerse en secreto y no incluirlo en el código dado que cualquiera que lo conozca podrá generar tokens válidos.
Una vez importados los valores se implementa una ruta POST
para simular el proceso de login, proceso que implementaremos en la siguiente entrega. En esta implementación siempre se devuelve un token válido generado con la función sign
de jsonwebtoken
. La función sign
tiene tres parámetros y una función de callback, el primero es el cuerpo o payload del token donde se puede incluir cualquier objeto. El segundo parámetro es la palabra clave con la que se filmará el token. El tercero, es un objeto opcional en el que se ha indicado en tiempo de expiración. Finalmente, en la función de callback, se comprueba si hay un error y si no es así se devuelve el token en un JSON.
Antes de poder usar esta ruta es necesario agregar al archivo routes\index.ts
la nueva ruta, por lo que el archivo deberá quedar de la siguiente manera.
import { Router } from 'express'; import auth from './auth'; import v0 from './v0'; const router = Router(); router.use('/auth', auth); router.use('/v0', v0); export default router;
Una vez hecho esto, el resultado de esta llamada se puede comprobar en Postman.
Middleware para la autenticación mediante JWT
Ahora una vez creado el token, es necesario crear un middleware para comprobar la validez del token y permitir o no el acceso al API. Para ello iremos a la carpeta middlewares
y en ella se creará el archivo verifytoken.ts
con el siguiente contenido.
import { Request, Response, NextFunction } from 'express'; import { verify } from 'jsonwebtoken'; const secret = String(process.env.TOKEN_SECRET); const verifytoken = (req: Request, res: Response, next: NextFunction) => { const token = req.header('x-access-token'); if (token == null) { return res.status(401).send(`No token specified`); } else { verify(token, secret, (err, payload) => { if (err) { return res.status(401).send(`The token is not valid`); } res.locals.payload = payload; next(); }); } }; export default verifytoken;
Al igual que en el caso anterior, lo primero que se hace después de importar las dependencias es importar el parámetro TOKEN_SECRET
del entorno. Posteriormente se implementa la función verifytoken()
que es lo que son los middlewares. En esta función lo primero que se hace es leer de la cabecera de la petición el valor x-access-token
donde se debe incluir el token. Si este valor no existe se devuelve un estado 401 indicando que no se ha especificado un token. Por otro lado, en el caso de que exista, se verificar mediante la función verify()
de jsonwebtoken
. Esta función tiene dos parámetros, el token y la palabra clave, y una función de callback para procesar la respuesta. Si el token no es válido se producirá un error, por lo que se devuelve un estado 401 indicado que el token no es válido. Posteriormente, si el token es invalido, se carga los datos del payload
en la respuesta para que los diferentes componentes puedan usar estos valores. Finalmente se indica mediante la función next()
que continúe con el siguiente paso.
Ruta para comprobar el contenido del JWT
Ahora se debe incluir el middleware solamente en las rutas que se desean proteger. Los middlewares que se han usado hasta ahora se han usado en todas las rutas, por lo que habían agregado agregan al crear el servidor en la clase Server
. En este caso se debe agregar en cada una de las rutas como segundo parámetro de la función.
A modo de ejemplo se puede crear en routes\auth.ts
una ruta para comprobar el contenido del payload. Lo que se puede conseguir agregando el siguiente código al archivo.
router.get('/info', [verifytoken], (_req: Request, res: Response) => { res.send(res.locals.payload); });
En el que se crea una nueva ruta /info
que devuelve el contenido de res.locals.payload
. Nótese que en esta ocasión se ha indicado un segundo parámetro en el que se pasa un vector con el middleware creado anteriormente. Así, al llamar a la ruta lo primero que se ejecutará será este middleware y, si en este se llama a la función next()
, continuará con el contenido de la ruta. En el vector que se pasa como segundo parámetro es posible indicar tanto middlewares como sean necesarios, ejecutándose por orden hasta que uno devuelva una respuesta.
Ahora se puede comprobar en postran lo que pasa si no indica un token.
Como se puede ver sale el error No token specified
. Por otro lado, si se indica un token invalido se obtendrá el mensaje The token is not valid
. Finalmente, si el token es válido y no ha caducado, se obtendrá como respuesta el contenido del payload.
Proteger las rutas mediante JWT
Ahora en el archivo routes/v0/users.ts
se puede agregar todas las rutas implementadas en entradas anteriores el middleware para conseguir autenticación mediante JWT.
Conclusiones
En esta entrada de la serie se ha visto cómo agregar autenticación mediante JWT a las rutas de nuestra API. Aunque en este caso cualquiera que conozca la ruta de login podrá obtener un token válido y acceder a los recursos. Para solucionar este problema se puede implementar una autenticación de usuarios, lo que se verá en la siguiente entrega.
Imagen de Tayeb MEZAHDIA en Pixabay