Защита на Node.js RESTful API с JSON Web Tokens

Замисляли ли сте се как работи удостоверяването? Какво се крие зад цялата сложност и абстракции. Всъщност нищо особено. Това е начин за криптиране на стойност, от своя страна създавайки уникален маркер, който потребителите използват като идентификатор. Този маркер потвърждава вашата самоличност. Той може да удостовери кой сте и да упълномощи различни ресурси, до които имате достъп. Ако случайно не познавате нито една от тези ключови думи, бъдете търпеливи, ще обясня всичко по-долу.

Това ще бъде урок стъпка по стъпка за това как да добавите удостоверяване, базирано на маркери, към съществуващ REST API. Въпросната стратегия за удостоверяване е JWT (JSON Web Token). Ако това не ви казва много, добре е. За мен беше също толкова странно, когато за първи път чух термина.

Какво всъщност означава JWT от гледна точка на земята? Нека разбием какво гласи официалната дефиниция:

JSON Web Token (JWT) е компактно, безопасно за URL адреси средство за представяне на искове, които трябва да бъдат прехвърлени между две страни. Вземанията в JWT са кодирани като JSON обект, който се използва като полезен товар на структура на JSON Web Signature (JWS) или като открит текст на структура на JSON Web Encryption (JWE), което позволява исковете да бъдат цифрово подписани или защитени от целостта с код за удостоверяване на съобщение (MAC) и / или криптиран.

- Работна група за интернет инженерство (IETF)

Това беше глътка. Нека преведем това на английски. JWT е кодиран низ от символи, който е безопасно за изпращане между два компютъра, ако и двамата имат HTTPS. Токенът представлява стойност, която е достъпна само от компютъра, който има достъп до секретния ключ, с който е шифрован. Достатъчно просто, нали?

Как изглежда това в реалния живот? Да предположим, че потребител иска да влезе в своя акаунт. Те изпращат до сървъра заявка с необходимите идентификационни данни като имейл и парола. Сървърът проверява дали идентификационните данни са валидни. Ако са, сървърът създава маркер, използвайки желания полезен товар и таен ключ. Този низ от символи, който е резултат от криптирането, се нарича жетон. След това сървърът го изпраща обратно на клиента. Клиентът от своя страна запазва маркера, за да го използва при всяка друга заявка, която потребителят ще изпрати. Практиката на добавяне на маркер към заглавията на заявките е като начин за упълномощаване на потребителя за достъп до ресурси. Това е практически пример за това как работи JWT.

Добре, стига толкова! Останалата част от този урок ще кодира и бих се радвал, ако следвате и кодирате заедно с мен, докато напредваме. Всеки фрагмент от код ще бъде последван от обяснение. Вярвам, че най-добрият начин да го разберете правилно ще бъде да го кодирате сами по пътя.

Преди да започна, има някои неща, които трябва да знаете за Node.js и някои стандарти на EcmaScript, които ще използвам. Няма да използвам ES6, тъй като не е толкова подходящ за начинаещи, колкото традиционния JavaScript. Но ще очаквам, че вече знаете как да изградите RESTful API с Node.js. Ако не, можете да заобиколите и да проверите това, преди да продължите.

Също така, цялата демонстрация е на GitHub, ако искате да я видите изцяло.

Нека започнем да пишем някакъв код, нали?

Е, все още не. Първо трябва да настроим средата. Кодът ще трябва да изчака поне още няколко минути. Тази част е скучна, така че за бързо стартиране и клониране ще клонираме хранилището от урока по-горе. Отворете прозореца на терминала или командния ред и изпълнете тази команда:

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

Ще видите папка, отворете я. Нека да разгледаме структурата на папките.

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

Имаме потребителска папка с модел и контролер и основна CRUD вече е внедрена. Нашият app.js съдържа основната конфигурация. На db.js прави, че свързва приложения към базата данни. В server.js гарантира нашия сървър се върти нагоре.

Продължете и инсталирайте всички необходими модули Node. Върнете се обратно към прозореца на терминала. Уверете се, че сте в папката с име ' nodejs-restful-api ' и стартирайте npm install. Изчакайте секунда или две, за да се инсталират модулите. Сега трябва да добавите низ свързване към база данни в db.js .

Преминете към mLab, създайте акаунт, ако все още нямате такъв, и отворете таблото за управление на базата данни. Създайте нова база данни, наименувайте я по ваше желание и продължете към нейната страница за конфигуриране. Добавете потребител на база данни към вашата база данни и копирайте низа на връзка от таблото за управление в своя код.

Всичко, което трябва да направите сега, е да промените стойностите на заместителите за и . Заменете ги с потребителското име и паролата на потребителя, който сте създали за базата данни. Подробно обяснение на този процес стъпка по стъпка може да се намери в урока, свързан по-горе.

Да предположим, че потребителят, който създадох за базата данни, е именуван wallyс парола на theflashisawesome. Имайки това предвид, файлът db.js сега трябва да изглежда по следния начин:

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

Продължете и завъртете сървъра обратно във вашия прозорец на терминала node server.js. Трябва да видите Express server listening on port 3000влизане в терминала.

И накрая, малко код.

Нека започнем с мозъчна атака за това, което искаме да изградим. На първо място искаме да добавим удостоверяване на потребителя. Значение, прилагане на система за регистриране и влизане на потребители в.

На второ място, искаме да добавим упълномощаване. Актът на предоставяне на разрешение на потребителите за достъп до определени ресурси в нашия REST API.

Започнете, като добавите нов файл в основната директория на проекта. Дайте му име config.js . Тук ще поставите конфигурационни настройки за приложението. Всичко, от което се нуждаем в момента, е само да дефинираме таен ключ за нашия JSON Web Token.

Отказ от отговорност : Имайте предвид, че при никакви обстоятелства никога (ВИНАГИ!) Не трябва публичният ви таен ключ да се вижда така. Винаги поставяйте всичките си ключове в променливи на околната среда! Пиша го по този начин само за демонстрационни цели.

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

С това добавяне сте готови да започнете да добавяте логика за удостоверяване. Създайте папка с име auth и започнете, като добавите файл с име AuthController.js . Този контролер ще бъде дом за нашата логика за удостоверяване.

Добавете тази част от кода в горната част на 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');

Сега сте готови да добавите модулите за използване на JSON Web Tokens и криптиране на пароли. Поставете този код в AuthController.js :

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

Отворете терминален прозорец във вашата папка на проекта и инсталирайте следните модули:

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

Това са всички модули, от които се нуждаем, за да приложим желаното ни удостоверяване. Сега сте готови да създадете /registerкрайна точка. Добавете тази част от кода към вашия 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); });
Функциите на Middleware са функции, които имат достъп до обекта на заявката (req), обекта за отговор (res) иnextфункцията в цикъла на заявката-отговор на приложението. Най-nextфункцията е функция на рутера Express, който при стартиране, изпълнява мидълуер успех текущата мидълуер.

- Използване на междинен софтуер, expressjs.com

Върнете се до пощальона и проверете какво се случва, когато ударите /api/auth/meкрайната точка. Изненадва ли ви, че резултатът е абсолютно същият? Трябва да бъде!

Отказ от отговорност : Продължете и изтрийте тази проба, преди да продължим, тъй като тя се използва само за демонстриране на логиката на използване 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.