Sikring af Node.js RESTfulde API'er med JSON Web Tokens

Har du nogensinde spekuleret på, hvordan godkendelse fungerer? Hvad ligger bag al kompleksitet og abstraktion. Faktisk ikke noget særligt. Det er en måde at kryptere en værdi på, hvilket igen skaber et unikt token, som brugerne bruger som en identifikator. Dette token bekræfter din identitet. Det kan godkende, hvem du er, og godkende forskellige ressourcer, du har adgang til. Hvis du med en chance ikke kender nogen af ​​disse nøgleord, vær tålmodig, jeg forklarer alt nedenfor.

Dette vil være en trinvis vejledning i, hvordan du tilføjer tokenbaseret godkendelse til en eksisterende REST API. Den pågældende godkendelsesstrategi er JWT (JSON Web Token). Hvis det ikke fortæller dig meget, er det fint. Det var lige så underligt for mig, da jeg første gang hørte ordet.

Hvad betyder JWT egentlig i et jordnær synspunkt? Lad os nedbryde, hvad den officielle definition siger:

JSON Web Token (JWT) er et kompakt, URL-sikkert middel til at repræsentere krav, der skal overføres mellem to parter. Påstandene i en JWT er kodet som et JSON-objekt, der bruges som nyttelasten til en JSON Web Signature (JWS) -struktur eller som almindelig tekst til en JSON Web Encryption (JWE) -struktur, hvilket gør det muligt for digitalt underskrevet eller integritetsbeskyttet krav. med en Message Authentication Code (MAC) og / eller krypteret.

- Internet Engineering Task Force (IETF)

Det var en mundfuld. Lad os oversætte det til engelsk. En JWT er en kodet streng af tegn, som det er sikkert at sende mellem to computere, hvis de begge har HTTPS. Tokenet repræsenterer en værdi, der kun er tilgængelig af den computer, der har adgang til den hemmelige nøgle, som den blev krypteret med. Simpelt nok, ikke?

Hvordan ser dette ud i det virkelige liv? Lad os sige, at en bruger vil logge ind på sin konto. De sender en anmodning med de nødvendige legitimationsoplysninger såsom e-mail og adgangskode til serveren. Serveren kontrollerer, om legitimationsoplysningerne er gyldige. Hvis de er det, opretter serveren et token ved hjælp af den ønskede nyttelast og en hemmelig nøgle. Denne streng af tegn, der skyldes kryptering kaldes et token. Derefter sender serveren det tilbage til klienten. Klienten gemmer igen tokenet for at bruge det i enhver anden anmodning, som brugeren sender. Praksis med at tilføje et token til anmodningsoverskrifterne er som en måde at give brugeren tilladelse til at få adgang til ressourcer. Dette er et praktisk eksempel på, hvordan JWT fungerer.

Okay, det er nok snak! Resten af ​​denne vejledning kodes, og jeg ville elske, at du ville følge med og kode ved siden af ​​mig, når vi skrider frem. Hvert kodestykke efterfølges af en forklaring. Jeg tror, ​​at den bedste måde at forstå det korrekt på er at kode det selv undervejs.

Før jeg begynder, er der nogle ting, du har brug for at vide om Node.js og nogle EcmaScript-standarder, jeg bruger. Jeg bruger ikke ES6, da den ikke er så nybegyndervenlig som traditionel JavaScript. Men jeg forventer, at du allerede ved, hvordan man bygger en RESTful API med Node.js. Hvis ikke, kan du tage en omvej og tjekke dette, inden du fortsætter.

Hele demoen er også på GitHub, hvis du ønsker at se den i sin helhed.

Lad os begynde at skrive noget kode, skal vi?

Nå, faktisk endnu ikke. Vi skal først oprette miljøet. Koden bliver nødt til at vente mindst et par minutter mere. Denne del er kedelig, så for at komme hurtigt i gang kloner vi lageret fra ovenstående tutorial. Åbn et terminalvindue eller kommandolinjeprompt, og kør denne kommando:

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

Du ser en mappe vises, åbn den. Lad os se på mappestrukturen.

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

Vi har en brugermappe med en model og en controller og grundlæggende CRUD allerede implementeret. Vores app.js indeholder den grundlæggende konfiguration. De db.js sørger for, at programmet opretter forbindelse til databasen. Den server.js sørger vores server spinder op.

Fortsæt og installer alle nødvendige Node-moduler. Skift tilbage til dit terminalvindue. Sørg for at du er i mappen med navnet ' nodejs-restful-api ' og kør npm install. Vent et sekund eller to, indtil modulerne installeres. Nu skal du tilføje en database forbindelse streng i db.js .

Spring over til mLab, opret en konto, hvis du ikke allerede har en, og åbn dit databasedashboard. Opret en ny database, navngiv den, som du ønsker, og fortsæt til dens konfigurationsside. Føj en databasebruger til din database, og kopier forbindelsesstrengen fra instrumentbrættet til din kode.

Alt hvad du skal gøre nu er at ændre pladsholderværdierne for og . Udskift dem med brugernavnet og adgangskoden til den bruger, du oprettede til databasen. En detaljeret forklaring trin for trin på denne proces kan findes i den tutorial, der er linket ovenfor.

Lad os sige, at den bruger, jeg oprettede til databasen, er navngivet wallymed en adgangskode på theflashisawesome. Når det er i tankerne, skal db.js- filen nu se sådan ud:

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

Gå videre og drej op på serveren, tilbage i din terminalvinduetype node server.js. Du skal se Express server listening on port 3000blive logget på terminalen.

Endelig noget kode.

Lad os starte med brainstorming om, hvad vi vil bygge. Først og fremmest vil vi tilføje brugergodkendelse. Betydning, implementering af et system til registrering og login af brugere.

For det andet vil vi tilføje autorisation. Handlingen med at give brugerne tilladelse til at få adgang til visse ressourcer i vores REST API.

Start med at tilføje en ny fil i rodmappen til projektet. Giv det navnet config.js . Her lægger du konfigurationsindstillinger til applikationen. Alt, hvad vi har brug for i øjeblikket, er bare for at definere en hemmelig nøgle til vores JSON Web Token.

Ansvarsfraskrivelse : Husk, under ingen omstændigheder bør du nogensinde (nogensinde!) Have din hemmelige nøgle offentligt synlig som denne. Sæt altid alle dine nøgler i miljøvariabler! Jeg skriver kun sådan til demo-formål.

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

Med dette tilføjet er du klar til at begynde at tilføje godkendelseslogikken. Opret en mappe med navnet auth, og start med at tilføje en fil med navnet AuthController.js . Denne controller vil være hjemme for vores godkendelseslogik.

Tilføj dette stykke kode øverst på 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');

Nu er du klar til at tilføje modulerne til brug af JSON Web Tokens og kryptering af adgangskoder. Indsæt denne kode i AuthController.js :

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

Åbn et terminalvindue i din projektmappe, og installer følgende moduler:

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

Det er alle de moduler, vi har brug for til at implementere vores ønskede godkendelse. Nu er du klar til at oprette et /registerslutpunkt. Føj dette stykke kode til din 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- funktioner er funktioner, der har adgang til anmodningsobjektet ( req), responsobjektet ( res) og nextfunktionen i programmets anmodnings-svar-cyklus. Den nextfunktion er en funktion i Express router, der, når påberåbes, udfører middleware efterfølgende den aktuelle middleware.

- Brug af middleware, expressjs.com

Gå tilbage til postbud og se, hvad der sker, når du rammer /api/auth/meslutpunktet. Overrasker det dig, at resultatet er nøjagtigt det samme? Det bør være!

Ansvarsfraskrivelse : Fortsæt og slet denne prøve, før vi fortsætter, da den kun bruges til at demonstrere brugen af ​​logikken 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- funktioner bruges som broer mellem nogle kodestykker. Når de bruges i funktionskæden til et slutpunkt, kan de være utroligt nyttige i autorisation og fejlhåndtering.

Håber jer og piger nød at læse dette lige så meget som jeg nød at skrive det. Indtil næste gang, vær nysgerrig og have det sjovt.

Tror du, at denne vejledning vil være til hjælp for nogen? Tøv ikke med at dele. Hvis du kunne lide det, bedes du klappe for mig.