Protección de las API RESTful de Node.js con tokens web JSON

¿Alguna vez se preguntó cómo funciona la autenticación? ¿Qué hay detrás de toda la complejidad y abstracciones? De hecho, nada especial. Es una forma de cifrar un valor, creando a su vez un token único que los usuarios utilizan como identificador. Este token verifica su identidad. Puede autenticar quién es usted y autorizar varios recursos a los que tiene acceso. Si por casualidad no conoce ninguna de estas palabras clave, tenga paciencia, le explicaré todo a continuación.

Este será un tutorial paso a paso sobre cómo agregar autenticación basada en token a una API REST existente. La estrategia de autenticación en cuestión es JWT (JSON Web Token). Si eso no te dice mucho, está bien. Fue tan extraño para mí cuando escuché por primera vez el término.

¿Qué significa JWT realmente desde un punto de vista realista? Analicemos lo que dice la definición oficial:

JSON Web Token (JWT) es un medio compacto y seguro para URL de representar reclamaciones que se transferirán entre dos partes. Las afirmaciones en un JWT se codifican como un objeto JSON que se utiliza como carga útil de una estructura JSON Web Signature (JWS) o como texto sin formato de una estructura JSON Web Encryption (JWE), lo que permite que las reclamaciones estén firmadas digitalmente o protegidas por integridad. con un código de autenticación de mensajes (MAC) y / o cifrado.

- Grupo de trabajo de ingeniería de Internet (IETF)

Eso fue un bocado. Traduzcamos eso al inglés. Un JWT es una cadena de caracteres codificada que es seguro enviar entre dos computadoras si ambas tienen HTTPS. El token representa un valor al que solo puede acceder la computadora que tiene acceso a la clave secreta con la que fue cifrado. Bastante simple, ¿verdad?

¿Cómo se ve esto en la vida real? Supongamos que un usuario quiere iniciar sesión en su cuenta. Envían una solicitud con las credenciales requeridas, como correo electrónico y contraseña, al servidor. El servidor comprueba si las credenciales son válidas. Si es así, el servidor crea un token utilizando la carga útil deseada y una clave secreta. Esta cadena de caracteres que resulta del cifrado se llama token. Luego, el servidor lo envía de vuelta al cliente. El cliente, a su vez, guarda el token para usarlo en todas las demás solicitudes que envíe el usuario. La práctica de agregar un token a los encabezados de la solicitud es una forma de autorizar al usuario a acceder a los recursos. Este es un ejemplo práctico de cómo funciona JWT.

Está bien, ¡ya es suficiente charla! El resto de este tutorial será de codificación, y me encantaría que me siguieran y codificaran a mi lado, a medida que avanzamos. Cada fragmento de código irá seguido de una explicación. Creo que la mejor manera de entenderlo correctamente será codificarlo usted mismo a lo largo del camino.

Antes de comenzar, hay algunas cosas que debe saber sobre Node.js y algunos estándares de EcmaScript que usaré. No usaré ES6, ya que no es tan amigable para principiantes como JavaScript tradicional. Pero, espero que ya sepa cómo construir una API RESTful con Node.js. De lo contrario, puede tomar un desvío y comprobarlo antes de continuar.

Además, toda la demostración está en GitHub si desea verla en su totalidad.

Empecemos a escribir código, ¿de acuerdo?

Bueno, todavía no. Primero tenemos que configurar el entorno. El código tendrá que esperar al menos un par de minutos más. Esta parte es aburrida, así que para ponernos en marcha rápidamente, clonaremos el repositorio del tutorial anterior. Abra una ventana de terminal o una línea de comandos y ejecute este comando:

git clone //github.com/adnanrahic/nodejs-restful-api.git

Verás aparecer una carpeta, ábrela. Echemos un vistazo a la estructura de carpetas.

> user - User.js - UserController.js - db.js - server.js - app.js - package.json

Tenemos una carpeta de usuario con un modelo y un controlador, y CRUD básico ya implementado. Nuestro app.js contiene la configuración básica. Los db.js asegura que la conecta a la base de datos de aplicación. El server.js se asegura de que nuestro servidor funcione .

Continúe e instale todos los módulos de nodo necesarios. Regrese a la ventana de su terminal. Asegúrese de estar en la carpeta llamada ' nodejs-restful-api ' y ejecute npm install. Espere uno o dos segundos para que se instalen los módulos. Ahora es necesario agregar una cadena de conexión de base de datos en db.js .

Vaya a mLab, cree una cuenta si aún no tiene una y abra el panel de su base de datos. Cree una nueva base de datos, asígnele el nombre que desee y proceda a su página de configuración. Agregue un usuario de la base de datos a su base de datos y copie la cadena de conexión del tablero a su código.

Todo lo que necesita hacer ahora es cambiar los valores de marcador de posición para y . Reemplácelos con el nombre de usuario y la contraseña del usuario que creó para la base de datos. Se puede encontrar una explicación detallada paso a paso de este proceso en el tutorial vinculado anteriormente.

Digamos que el nombre del usuario que creé para la base de datos tiene la wallycontraseña theflashisawesome. Teniendo esto en cuenta, el archivo db.js ahora debería verse así:

var mongoose = require('mongoose'); mongoose.connect('mongodb://wally:[email protected]:47072/securing-rest-apis-with-jwt', { useMongoClient: true });

Continúe y active el servidor, de nuevo en su tipo de ventana de terminal node server.js. Debería ver Express server listening on port 3000iniciar sesión en la terminal.

Finalmente, algo de código.

Comencemos con una lluvia de ideas sobre lo que queremos construir. En primer lugar, queremos agregar la autenticación de usuario. Es decir, implementar un sistema para registrar e iniciar sesión de usuarios.

En segundo lugar, queremos agregar autorización. El acto de otorgar a los usuarios el permiso para acceder a ciertos recursos en nuestra API REST.

Comience agregando un nuevo archivo en el directorio raíz del proyecto. Dale un nombre de config.js . Aquí pondrás los ajustes de configuración para la aplicación. Todo lo que necesitamos en este momento es solo definir una clave secreta para nuestro JSON Web Token.

Descargo de responsabilidad : tenga en cuenta que bajo ninguna circunstancia debe (¡NUNCA!) Tener su clave secreta visible públicamente de esta manera. ¡Siempre ponga todas sus claves en variables de entorno! Solo lo escribo así para fines de demostración.

// config.js module.exports = { 'secret': 'supersecret' };

Con esto agregado, está listo para comenzar a agregar la lógica de autenticación. Cree una carpeta llamada auth y comience agregando un archivo llamado AuthController.js . Este controlador será el hogar de nuestra lógica de autenticación.

Agregue este fragmento de código en la parte superior de AuthController.js .

// AuthController.js var express = require('express'); var router = express.Router(); var bodyParser = require('body-parser'); router.use(bodyParser.urlencoded({ extended: false })); router.use(bodyParser.json()); var User = require('../user/User');

Ahora está listo para agregar los módulos para usar JSON Web Tokens y encriptar contraseñas. Pegue este código en AuthController.js :

var jwt = require('jsonwebtoken'); var bcrypt = require('bcryptjs'); var config = require('../config');

Abra una ventana de terminal en la carpeta de su proyecto e instale los siguientes módulos:

npm install jsonwebtoken --save npm install bcryptjs --save

Esos son todos los módulos que necesitamos para implementar nuestra autenticación deseada. Ahora está listo para crear un /registerpunto final. Agregue este fragmento de código a su AuthController.js :

router.post('/register', function(req, res) { var hashedPassword = bcrypt.hashSync(req.body.password, 8); User.create({ name : req.body.name, email : req.body.email, password : hashedPassword }, function (err, user) { if (err) return res.status(500).send("There was a problem registering the user.") // create a token var token = jwt.sign({ id: user._id }, config.secret, { expiresIn: 86400 // expires in 24 hours }); res.status(200).send({ auth: true, token: token }); }); });

Here we’re expecting the user to send us three values, a name, an email and a password. We’re immediately going to take the password and encrypt it with Bcrypt’s hashing method. Then take the hashed password, include name and email and create a new user. After the user has been successfully created, we’re at ease to create a token for that user.

The jwt.sign() method takes a payload and the secret key defined in config.js as parameters. It creates a unique string of characters representing the payload. In our case, the payload is an object containing only the id of the user. Let’s write a piece of code to get the user id based on the token we got back from the register endpoint.

router.get('/me', function(req, res) { var token = req.headers['x-access-token']; if (!token) return res.status(401).send({ auth: false, message: 'No token provided.' }); jwt.verify(token, config.secret, function(err, decoded) { if (err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' }); res.status(200).send(decoded); }); });

Here we’re expecting the token be sent along with the request in the headers. The default name for a token in the headers of an HTTP request is x-access-token. If there is no token provided with the request the server sends back an error. To be more precise, an 401 unauthorized status with a response message of No token provided. If the token exists, the jwt.verify() method will be called. This method decodes the token making it possible to view the original payload. We’ll handle errors if there are any and if there are not, send back the decoded value as the response.

Finally we need to add the route to the AuthController.js in our main app.js file. First export the router from AuthController.js:

// add this to the bottom of AuthController.js module.exports = router;

Then add a reference to the controller in the main app, right above where you exported the app.

// app.js var AuthController = require('./auth/AuthController'); app.use('/api/auth', AuthController); module.exports = app;

Let’s test this out. Why not?

Open up your REST API testing tool of choice, I use Postman or Insomnia, but any will do.

Go back to your terminal and run node server.js. If it is running, stop it, save all changes to you files, and run node server.js again.

Open up Postman and hit the register endpoint (/api/auth/register). Make sure to pick the POST method and x-www-form-url-encoded. Now, add some values. My user’s name is Mike and his password is ‘thisisasecretpassword’. That’s not the best password I’ve ever seen, to be honest, but it’ll do. Hit send!

See the response? The token is a long jumbled string. To try out the /api/auth/me endpoint, first copy the token. Change the URL to /me instead of /register, and the method to GET. Now you can add the token to the request header.

Voilà! The token has been decoded into an object with an id field. Want to make sure that the id really belongs to Mike, the user we just created? Sure you do. Jump back into your code editor.

// in AuthController.js change this line res.status(200).send(decoded); // to User.findById(decoded.id, function (err, user) { if (err) return res.status(500).send("There was a problem finding the user."); if (!user) return res.status(404).send("No user found."); res.status(200).send(user); });

Now when you send a request to the /me endpoint you’ll see:

The response now contains the whole user object! Cool! But, not good. The password should never be returned with the other data about the user. Let’s fix this. We can add a projection to the query and omit the password. Like this:

User.findById(decoded.id, { password: 0 }, // projection function (err, user) { if (err) return res.status(500).send("There was a problem finding the user."); if (!user) return res.status(404).send("No user found."); res.status(200).send(user); });

That’s better, now we can see all values except the password. Mike’s looking good.

Did someone say login?

After implementing the registration, we should create a way for existing users to log in. Let’s think about it for a second. The register endpoint required us to create a user, hash a password, and issue a token. What will the login endpoint need us to implement? It should check if a user with the given email exists at all. But also check if the provided password matches the hashed password in the database. Only then will we want to issue a token. Add this to your AuthController.js.

router.post('/login', function(req, res) { User.findOne({ email: req.body.email }, function (err, user) { if (err) return res.status(500).send('Error on the server.'); if (!user) return res.status(404).send('No user found.'); var passwordIsValid = bcrypt.compareSync(req.body.password, user.password); if (!passwordIsValid) return res.status(401).send({ auth: false, token: null }); var token = jwt.sign({ id: user._id }, config.secret, { expiresIn: 86400 // expires in 24 hours }); res.status(200).send({ auth: true, token: token }); }); });

First of all we check if the user exists. Then using Bcrypt’s .compareSync() method we compare the password sent with the request to the password in the database. If they match we .sign() a token. That’s pretty much it. Let’s try it out.

Cool it works! What if we get the password wrong?

Great, when the password is wrong the server sends a response status of 401 unauthorized. Just what we wanted!

To finish off this part of the tutorial, let’s add a simple logout endpoint to nullify the token.

// AuthController.js router.get('/logout', function(req, res) { res.status(200).send({ auth: false, token: null }); });

Disclaimer: The logout endpoint is not needed. The act of logging out can solely be done through the client side. A token is usually kept in a cookie or the browser’s localstorage. Logging out is as simple as destroying the token on the client. This /logout endpoint is created to logically depict what happens when you log out. The token gets set to null.

With this we’ve finished the authentication part of the tutorial. Want to move on to the authorization? I bet you do.

Do you have permission to be here?

To comprehend the logic behind an authorization strategy we need to wrap our head around something called middleware. Its name is self explanatory, to some extent, isn’t it? Middleware is a piece of code, a function in Node.js, that acts as a bridge between some parts of your code.

When a request reaches an endpoint, the router has an option to pass the request on to the next middleware function in line. Emphasis on the word next! Because that’s exactly what the name of the function is! Let’s see an example. Comment out the line where you send back the user as a response. Add a next(user) right underneath.

router.get('/me', function(req, res, next) { var token = req.headers['x-access-token']; if (!token) return res.status(401).send({ auth: false, message: 'No token provided.' }); jwt.verify(token, config.secret, function(err, decoded) { if (err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' }); User.findById(decoded.id, { password: 0 }, // projection function (err, user) { if (err) return res.status(500).send("There was a problem finding the user."); if (!user) return res.status(404).send("No user found."); // res.status(200).send(user); Comment this out! next(user); // add this line }); }); }); // add the middleware function router.use(function (user, req, res, next) { res.status(200).send(user); });
Las funciones de middleware son funciones que tienen acceso al objeto de solicitud ( req), el objeto de respuesta ( res) y la nextfunción en el ciclo de solicitud-respuesta de la aplicación. La nextfunción es una función en el enrutador Express que, cuando se invoca, ejecuta el middleware sucediendo al middleware actual.

- Usando middleware, expressjs.com

Vuelve al cartero y mira lo que sucede cuando llegas al /api/auth/mepunto final. ¿Le sorprende que el resultado sea exactamente el mismo? ¡Debería ser!

Descargo de responsabilidad : continúe y elimine esta muestra antes de continuar, ya que solo se usa para demostrar la lógica del uso next().

Let’s take this same logic and apply it to create a middleware function to check the validity of tokens. Create a new file in the auth folder and name it VerifyToken.js. Paste this snippet of code in there.

var jwt = require('jsonwebtoken'); var config = require('../config'); function verifyToken(req, res, next) { var token = req.headers['x-access-token']; if (!token) return res.status(403).send({ auth: false, message: 'No token provided.' }); jwt.verify(token, config.secret, function(err, decoded) { if (err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' }); // if everything good, save to request for use in other routes req.userId = decoded.id; next(); }); } module.exports = verifyToken;

Let’s break it down. We’re going to use this function as a custom middleware to check if a token exists and whether it is valid. After validating it, we add the decoded.id value to the request (req) variable. We now have access to it in the next function in line in the request-response cycle. Calling next() will make sure flow will continue to the next function waiting in line. In the end, we export the function.

Now, open up the AuthController.js once again. Add a reference to VerifyToken.js at the top of the file and edit the /me endpoint. It should now look like this:

// AuthController.js var VerifyToken = require('./VerifyToken'); // ... router.get('/me', VerifyToken, function(req, res, next) { User.findById(req.userId, { password: 0 }, function (err, user) { if (err) return res.status(500).send("There was a problem finding the user."); if (!user) return res.status(404).send("No user found."); res.status(200).send(user); }); }); // ...

See how we added VerifyToken in the chain of functions? We now handle all the authorization in the middleware. This frees up all the space in the callback to only handle the logic we need. This is an awesome example of how to write DRY code. Now, every time you need to authorize a user you can add this middleware function to the chain. Test it in Postman again, to make sure it still works like it should.

Feel free to mess with the token and try the endpoint again. With an invalid token, you’ll see the desired error message, and be sure the code you wrote works the way you want.

Why is this so powerful? You can now add the VerifyTokenmiddleware to any chain of functions and be sure the endpoints are secured. Only users with verified tokens can access the resources!

Wrapping your head around everything.

Don’t feel bad if you did not grasp everything at once. Some of these concepts are hard to understand. It’s fine to take a step back and rest your brain before trying again. That’s why I recommend you go through the code by yourself and try your best to get it to work.

Again, here’s the GitHub repository. You can catch up on any things you may have missed, or just get a better look at the code if you get stuck.

Remember, authentication is the act of logging a user in. Authorization is the act of verifying the access rights of a user to interact with a resource.

Middleware functions are used as bridges between some pieces of code. When used in the function chain of an endpoint they can be incredibly useful in authorization and error handling.

Hope you guys and girls enjoyed reading this as much as I enjoyed writing it. Until next time, be curious and have fun.

Do you think this tutorial will be of help to someone? Do not hesitate to share. If you liked it, please clap for me.