Sichern von Node.js RESTful-APIs mit JSON-Web-Token

Haben Sie sich jemals gefragt, wie die Authentifizierung funktioniert? Was steckt hinter all der Komplexität und Abstraktion? Eigentlich nichts Besonderes. Auf diese Weise können Sie einen Wert verschlüsseln und ein eindeutiges Token erstellen, das Benutzer als Kennung verwenden. Dieses Token überprüft Ihre Identität. Es kann authentifizieren, wer Sie sind, und verschiedene Ressourcen autorisieren, auf die Sie Zugriff haben. Wenn Sie zufällig keines dieser Schlüsselwörter kennen, haben Sie etwas Geduld, ich werde alles unten erklären.

Dies ist eine schrittweise Anleitung zum Hinzufügen einer tokenbasierten Authentifizierung zu einer vorhandenen REST-API. Die fragliche Authentifizierungsstrategie ist JWT (JSON Web Token). Wenn dir das nicht viel sagt, ist es in Ordnung. Es war genauso seltsam für mich, als ich den Begriff zum ersten Mal hörte.

Was bedeutet JWT eigentlich aus bodenständiger Sicht? Lassen Sie uns zusammenfassen, was die offizielle Definition besagt:

JSON Web Token (JWT) ist ein kompaktes, URL-sicheres Mittel zur Darstellung von Ansprüchen, die zwischen zwei Parteien übertragen werden sollen. Die Ansprüche in einem JWT werden als JSON-Objekt codiert, das als Nutzlast einer JON-Struktur (JSON Web Signature) oder als Klartext einer JWE-Struktur (JSON Web Encryption) verwendet wird, sodass die Ansprüche digital signiert oder integer geschützt werden können mit einem Nachrichtenauthentifizierungscode (MAC) und / oder verschlüsselt.

- Internet Engineering Task Force (IETF)

Das war ein Schluck. Lassen Sie uns das ins Englische übersetzen. Ein JWT ist eine codierte Zeichenfolge, die sicher zwischen zwei Computern gesendet werden kann, wenn beide über HTTPS verfügen. Das Token stellt einen Wert dar, auf den nur der Computer zugreifen kann, der Zugriff auf den geheimen Schlüssel hat, mit dem er verschlüsselt wurde. Einfach genug, oder?

Wie sieht das im wirklichen Leben aus? Angenommen, ein Benutzer möchte sich in seinem Konto anmelden. Sie senden eine Anfrage mit den erforderlichen Anmeldeinformationen wie E-Mail und Passwort an den Server. Der Server prüft, ob die Anmeldeinformationen gültig sind. Wenn dies der Fall ist, erstellt der Server ein Token mit der gewünschten Nutzlast und einem geheimen Schlüssel. Diese Zeichenfolge, die sich aus der Verschlüsselung ergibt, wird als Token bezeichnet. Dann sendet der Server es zurück an den Client. Der Client speichert seinerseits das Token, um es in jeder anderen vom Benutzer gesendeten Anforderung zu verwenden. Das Hinzufügen eines Tokens zu den Anforderungsheadern dient dazu, den Benutzer zum Zugriff auf Ressourcen zu autorisieren. Dies ist ein praktisches Beispiel für die Funktionsweise von JWT.

Okay, das ist genug geredet! Der Rest dieses Tutorials wird das Codieren sein, und ich würde mich freuen, wenn Sie mir folgen und im weiteren Verlauf codieren würden. Auf jeden Codeausschnitt folgt eine Erklärung. Ich glaube, der beste Weg, es richtig zu verstehen, besteht darin, es selbst zu codieren.

Bevor ich anfange, müssen Sie einige Dinge über Node.js und einige EcmaScript-Standards wissen, die ich verwenden werde. Ich werde ES6 nicht verwenden, da es nicht so anfängerfreundlich ist wie traditionelles JavaScript. Ich gehe jedoch davon aus, dass Sie bereits wissen, wie Sie mit Node.js eine RESTful-API erstellen. Wenn nicht, können Sie einen Umweg machen und dies überprüfen, bevor Sie fortfahren.

Außerdem ist die gesamte Demo auf GitHub verfügbar, wenn Sie sie vollständig sehen möchten.

Beginnen wir mit dem Schreiben von Code.

Nun, eigentlich noch nicht. Wir müssen zuerst die Umgebung einrichten. Der Code muss noch mindestens ein paar Minuten warten. Dieser Teil ist langweilig. Um schnell einsatzbereit zu sein, klonen wir das Repository aus dem obigen Tutorial. Öffnen Sie ein Terminalfenster oder eine Eingabeaufforderung und führen Sie diesen Befehl aus:

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

Sie sehen einen Ordner, öffnen Sie ihn. Werfen wir einen Blick auf die Ordnerstruktur.

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

Wir haben einen Benutzerordner mit einem Modell und einem Controller und eine grundlegende CRUD bereits implementiert. Unsere app.js enthält die Grundkonfiguration. Die Datei db.js stellt sicher, dass die Anwendung eine Verbindung zur Datenbank herstellt. Die Datei server.js stellt sicher, dass unser Server hochfährt .

Installieren Sie alle erforderlichen Knotenmodule. Wechseln Sie zurück zu Ihrem Terminalfenster. Stellen Sie sicher, dass Sie sich im Ordner ' nodejs-restful-api ' befinden, und führen Sie ihn aus npm install. Warten Sie ein oder zwei Sekunden, bis die Module installiert sind. Jetzt müssen Sie eine Datenbankverbindungszeichenfolge in db.js hinzufügen .

Wechseln Sie zu mLab, erstellen Sie ein Konto, falls Sie noch keines haben, und öffnen Sie Ihr Datenbank-Dashboard. Erstellen Sie eine neue Datenbank, benennen Sie sie wie gewünscht und fahren Sie mit der Konfigurationsseite fort. Fügen Sie Ihrer Datenbank einen Datenbankbenutzer hinzu und kopieren Sie die Verbindungszeichenfolge aus dem Dashboard in Ihren Code.

Jetzt müssen Sie nur noch die Platzhalterwerte für und ändern . Ersetzen Sie sie durch den Benutzernamen und das Kennwort des Benutzers, den Sie für die Datenbank erstellt haben. Eine ausführliche schrittweise Erklärung dieses Prozesses finden Sie im oben verlinkten Tutorial.

Angenommen, der Benutzer, den ich für die Datenbank erstellt habe, trägt das wallyKennwort theflashisawesome. Vor diesem Hintergrund sollte die Datei db.js nun ungefähr so aussehen:

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

Fahren Sie fort und starten Sie den Server wieder in Ihrem Terminalfenstertyp node server.js. Sie sollten sehen, Express server listening on port 3000dass Sie am Terminal angemeldet werden.

Zum Schluss noch ein Code.

Beginnen wir mit einem Brainstorming darüber, was wir bauen wollen. Zunächst möchten wir die Benutzerauthentifizierung hinzufügen. Das heißt, Implementierung eines Systems zum Registrieren und Anmelden von Benutzern.

Zweitens möchten wir die Autorisierung hinzufügen. Der Vorgang, Benutzern die Berechtigung zum Zugriff auf bestimmte Ressourcen in unserer REST-API zu erteilen.

Fügen Sie zunächst eine neue Datei im Stammverzeichnis des Projekts hinzu. Geben Sie ihm den Namen config.js . Hier legen Sie die Konfigurationseinstellungen für die Anwendung fest. Im Moment brauchen wir nur einen geheimen Schlüssel für unser JSON-Web-Token zu definieren.

Haftungsausschluss : Denken Sie daran, dass Sie unter keinen Umständen (NIEMALS!) Ihren geheimen Schlüssel so öffentlich sichtbar machen sollten. Setzen Sie immer alle Ihre Schlüssel in Umgebungsvariablen! Ich schreibe es nur so für Demozwecke.

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

Mit diesem Zusatz können Sie mit dem Hinzufügen der Authentifizierungslogik beginnen. Erstellen Sie einen Ordner mit dem Namen auth und fügen Sie zunächst eine Datei mit dem Namen AuthController.js hinzu . Dieser Controller ist die Heimat unserer Authentifizierungslogik.

Fügen Sie diesen Code oben in AuthController.js hinzu .

// 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');

Jetzt können Sie die Module für die Verwendung von JSON-Web-Tokens und die Verschlüsselung von Kennwörtern hinzufügen. Fügen Sie diesen Code in AuthController.js ein :

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

Öffnen Sie ein Terminalfenster in Ihrem Projektordner und installieren Sie die folgenden Module:

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

Das sind alle Module, die wir benötigen, um unsere gewünschte Authentifizierung zu implementieren. Jetzt können Sie einen /registerEndpunkt erstellen . Fügen Sie diesen Code zu AuthController.js hinzu :

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- Funktionen sind Funktionen, die Zugriff auf das Anforderungsobjekt (req), das Antwortobjekt (res) und dienextFunktion im Anforderungs- / Antwortzyklus der Anwendung haben. DienextFunktion ist eine Funktion im Express-Router, die beim Aufrufen die Middleware ausführt, die der aktuellen Middleware folgt.

- Mit Middleware, expressjs.com

Springen Sie zurück zum Postboten und überprüfen Sie, was passiert, wenn Sie den /api/auth/meEndpunkt erreichen. Überrascht es Sie, dass das Ergebnis genau das gleiche ist? Es sollte sein!

Haftungsausschluss : Löschen Sie dieses Beispiel, bevor Sie fortfahren, da es nur zur Demonstration der Verwendungslogik verwendet wird 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.