Komplettes Login System mit Node.js & Vue.js | RestAPI & JWT | Part [1/2]

Komplettes Login System mit Node.js & Vue.js | RestAPI & JWT | Part [1/2]

In diesem Beitrag erfährst Du, wie Du mit Node.js, JWT (JSON Web Tokens) und MySQL eine eigene RestAPI für die Authentifizierung von Benutzern – also ein komplettes Login System – erstellen kannst.

Dieser Beitrag ist der erste Teil der zweiteiligen Serie Komplettes Login System mit Node.js & Vue.js. Wir kümmern uns hier um die Umsetzung eines kompletten Login Systems mit Node.js als Backend und Vue.js als Frontend. Das Tutorial ist in zwei Beiträge aufgeteilt, damit Du nicht an ein Vue.js Frontend gebunden bist, sondern die RestAPI, die wir in diesem Beitrag entwickeln genau so gut auf andere Frontends, wie Angular oder React anwenden kannst.

➡️ Teil 2: Komplettes Login System mit Node.js & Vue.js | Vuex

Unser Ziel für Teil 1: RestAPI mit JWT und MySQL

Wir erstellen eine Node.js Anwendung, die auf unserem lokalen Server läuft. Dazu haben wir eine MySQL Datenbank, in der unsere Benutzerdaten gespeichert sind. Zur Authentifizierung müssen wir diese Daten dann abfragen und können mit Hilfe der JWT Erweiterung eine Session (Sitzung) für den Benutzer eröffnen.

Am Ende hast Du eine lauffähige Anwendung, die mit Hilfe dieser Anleitung auf deinem eigenen Server deployen (live stellen) kannst. Aber nun wollen wir endlich starten! 🙂

Tasse mit Aufschrift

1. Was ist eine RestAPI?

Eine RestAPI stellt die Schnittstelle zwischen Server und Client dar. Über normale HTTP Anfragen erreichen wir den Server und können programmierte Funktionen ausführen, wie das Authentifizieren eines Benutzers mit zugehörigem Passwort.

Da dieses Tutorial nicht für absolute Einsteiger geeignet ist, gehe ich davon aus, dass Du bereits mit Node.js etwas vertraut bist, weshalb wir die Installation überspringen und direkt zum spannenden Teil kommen. Sollte das nicht der Fall sein, findest Du hier eine gute Anleitung für den Einstieg mit Node.js.

2. Abhängigkeiten installieren

Unsere Node.js App ist also bereit für die Installation der Abhängigkeiten. Wir benötigen folgende Module:

expressDamit erstellen wir unseren eigenen Webserver für unsere RestAPI
body-parserEinfachere Handhabung von Middleware für Requests
mysqlZum Lesen/Schreiben in unsere Datenbank
uuidZum Erstellen von IDs für spätere Benutzer
bcryptjs (Achtung: nicht bcrypt)Zum Ver-/Entschlüsseln der Passwörter
jsonwebtokenZur Handhabung der Nutzer-Sessions
corsDamit wir die RestAPI von unserer Website aufrufen können.

Diese Module installieren wir über folgenden CLI Befehl:

npm install bcryptjs body-parser express jsonwebtoken mysql uuid cors

3. Datenbank einrichten

Für die Datenbank nutze ich XAMPP, damit ich mir eine eigene Datenbank lokal hosten kann. Natürlich kannst Du auch jede andere (Remote-) Datenbank verwenden.

Für unser Login System benötigen wir nur eine Tabelle nach folgendem Schema:

users - ER model

In der Datenbank sieht unsere Tabelle dann folgendermaßen aus:

phpmyadmin - table

Damit wir auch über unsere Node.js Anwendung auf diese Verbindung zugreifen können, erstellen wir eine eigene Klasse Datei, die wir später in unserem Router einbinden.

// lib/db.js

const mysql = require('mysql');

const connection = mysql.createConnection({
	host: 'localhost',
	user: 'node-jwt',
	database: 'node-jwt',
	password: '********'
});

connection.connect();
module.exports = connection;

4. Express Router aufsetzen und Routen erstellen

Unsere Einstiegsdatei ist die index.js und beinhaltet das Starten unsere Webservers und die Einbindung der Routen, die wir in der Datei routes/router.js definieren.

// index.js

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const cors = require('cors');

// set up port
const PORT = process.env.PORT || 3000;

app.use(bodyParser.json());
app.use(cors());

// add routes
const router = require('./routes/router.js');
app.use('/api', router);

// run server
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

In der router.js definieren wir unsere Routen und verpacken danach die Logik darin. Grund, wieso wir hier eine extra Datei benutzen ist die Übersichtlichkeit. Wenn Deine Anwendung irgendwann 20 oder noch mehr Routen hat, gibt es ein großes Chaos in der index.js. Deshalb lagern wir unsere Routen aus.

// routes/router.js

const express = require('express');
const router = express.Router();

const bcrypt = require('bcryptjs');
const uuid = require('uuid');
const jwt = require('jsonwebtoken');

const db = require('../lib/db.js');
const userMiddleware = require('../middleware/users.js');

router.post('/sign-up', (req, res, next) => {});

router.post('/login', (req, res, next) => {});

router.get('/secret-route', (req, res, next) => {
	res.send('This is the secret content. Only logged in users can see that!');
});

module.exports = router;

Hier registrieren wir die Route /api/sign-up zum Registrieren und /api/login zum Anmelden. Außerdem haben wir die Route /secret-route, diese soll man nur aufrufen können, wenn man angemeldet ist. Aktuell kann diese jeder Benutzer aufrufen. Dazu aber später mehr.

In Zeile 10 binden wir außerdem unsere Datei für die Datenbank Verbindung ein.

Wir binden noch die Datei ../middleware/users.js ein, in der befindet sich der Code um die Anfragen zu verifizieren. Das bedeutet, wir prüfen dort, ob der Benutzer z.B. ein Passwort eingegeben hat und der Benutzername den Richtlinien entspricht. Diese Abfragen schalten wir später als Middleware in den Aufruf unserer Routen.

5. Middleware erstellen (Validierung)

Eine Middleware ist quasi ein kleines Programm, was zwischen zwei Komponenten geschaltet ist. In diesem Fall haben wir zwischen unserem Request und der eigentlichen Registrierung eine Middleware, die die eingegebenen Daten validiert. Für die Registrierung kann eine Validierung wie folgt aussehen:

// middleware/users.js

module.exports = {
	validateRegister: (req, res, next) => {
		// username min length 3
		if (!req.body.username || req.body.username.length < 3) {
			return res.status(400).send({
				msg: 'Please enter a username with min. 3 chars'
			});
		}

		// password min 6 chars
		if (!req.body.password || req.body.password.length < 6) {
			return res.status(400).send({
				msg: 'Please enter a password with min. 6 chars'
			});
		}

		// password (repeat) does not match
		if (
			!req.body.password_repeat ||
			req.body.password != req.body.password_repeat
		) {
			return res.status(400).send({
				msg: 'Both passwords must match'
			});
		}

		next();
	}
};

Beim Aufruf unserer /sign-up Route soll unsere Middleware ausgeführt werden. Dazu ändern wie die markierte Zeile wie folgt ab:

// routes/router.js

const express = require('express');
const router = express.Router();

const userMiddleware = require('../middleware/users.js');

router.post('sign-up', userMiddleware.validateRegister, (req, res, next) => {});

router.post('login', (req, res, next) => {});

module.exports = router;

6. Register (/sign-up) Route umsetzen (mit JWT)

Um einen neuen Benutzer in die Datenbank aufzunehmen, müssen wir überprüfen, ob der Benutzername noch nicht existiert. Ist der Benutzer vorhanden, wird eine Fehlermeldung ausgegeben. Ist der Benutzer noch nicht vorhanden, wird mittels unserem Modul bcrypt, das eingegebene Passwort gehasht (verschlüsselt) und dann alle Daten in die Datenbank eingetragen.

// routes/router.js

router.post('/sign-up', userMiddleware.validateRegister, (req, res, next) => {
  db.query(
    `SELECT * FROM users WHERE LOWER(username) = LOWER(${db.escape(
			req.body.username
		)});`,
    (err, result) => {
      if (result.length) {
        return res.status(409).send({
          msg: 'This username is already in use!'
        });
      } else {
        // username is available
        bcrypt.hash(req.body.password, 10, (err, hash) => {
          if (err) {
            return res.status(500).send({
              msg: err
            });
          } else {
            // has hashed pw => add to database
            db.query(
              `INSERT INTO users (id, username, password, registered) VALUES ('${uuid.v4()}', ${db.escape(
								req.body.username
							)}, ${db.escape(hash)}, now())`,
              (err, result) => {
                if (err) {
                  throw err;
                  return res.status(400).send({
                    msg: err
                  });
                }
                return res.status(201).send({
                  msg: 'Registered!'
                });
              }
            );
          }
        });
      }
    }
  );
});

Wichtig ist die Funktion db.escape(), beispielsweise in Zeile 23. Hiermit werden übergebene Parameter maskiert, um eine SQL Injection zu vermeiden. Ist das Eintragen des Benutzers erfolgreich, wird der Statuscode 201 („created“) zurückgegeben und der Funktionsaufruf damit beendet.

7. Login (/login) Route umsetzen (mit JWT)

Neben dem Registrierungsprozess haben wir eine Login Route, um sich für bereits registrierte Benutzer anzumelden. Hier wird der passende Datenbankeintrag anhand des Benutzernamen gesucht. Danach wird das eingegebene mit dem verschlüsselten Passwort aus der Datenbank mit Hilfe von jwt.compare() überprüft. Ein kurzes SQL Query setzt in Zeile 44 das letzt Login Datum/Zeit auf den aktuellen Wert.

// routes/router.js

router.post('/login', (req, res, next) => {
  db.query(
    `SELECT * FROM users WHERE username = ${db.escape(req.body.username)};`,
    (err, result) => {
      // user does not exists
      if (err) {
        throw err;
        return res.status(400).send({
          msg: err
        });
      }

      if (!result.length) {
        return res.status(401).send({
          msg: 'Username or password is incorrect!'
        });
      }

      // check password
      bcrypt.compare(
        req.body.password,
        result[0]['password'],
        (bErr, bResult) => {
          // wrong password
          if (bErr) {
            throw bErr;
            return res.status(401).send({
              msg: 'Username or password is incorrect!'
            });
          }

          if (bResult) {
            const token = jwt.sign({
                username: result[0].username,
                userId: result[0].id
              },
              'SECRETKEY', {
                expiresIn: '7d'
              }
            );

            db.query(
              `UPDATE users SET last_login = now() WHERE id = '${result[0].id}'`
            );
            return res.status(200).send({
              msg: 'Logged in!',
              token,
              user: result[0]
            });
          }
          return res.status(401).send({
            msg: 'Username or password is incorrect!'
          });
        }
      );
    }
  );
});

In Zeile 36 und 37 übergeben wir Variablen, die wir im JWT Token „speichern“ möchten. Dadurch haben wir in den geschützten Routen Zugriff auf diese Variablen.

In Zeile 39 musst Du einen Key übergeben, mit dem der JWT Token generiert wird, dieser ist später für die Überprüfung wichtig. Hier kannst Du einen beliebigen String eingeben.

Außerdem kannst Du in Zeile 40 festlegen, wie lange der Token gültig sein soll. Werte wie „1h“ oder „3m“ sind hier gültig. Die einzelnen Werte und Parameter kannst Du auch in der Dokumentation nachlesen.

Falls das Passwort falsch ist oder der Benutzername nicht existiert wird eine Fehlermeldung ausgegeben. Diese Meldung ist absichtlich identisch, da ein potentieller Angreifer sonst Informationen über das Existieren von einzelnen Benutzerprofilen bekommen kann.

Bei erfolgreichem Login wird das Benutzerobjekt und der von JWT generierte Token zurückgegeben. Dieser Token ist für alle Routen wichtig, in denen man eingeloggt sein soll. Im Teil 2 (Vue.js Frontend) erfährst Du, wie Du diesen bei jedem Request übergeben kannst. Wenn Du die RestAPI mit Postman testest, kannst Du den Token mit dem Key „Authorization“ als Value nach folgender Syntax angeben: „Bearer KEY“.

Postman Authorization Header

8. Routen mit Login schützen

Die wichtigsten Routen sind nun fertig. Wir können neue Benutzer hinzufügen und uns mit bestehenden Accounts anmelden. Nun wollen wir noch Routen schützen. Das bedeutet, dass nur angemeldete Benutzer Zugriff darauf haben.

Dazu legen wir in unserer users.js eine neue Middleware an. Hierbei wird der Token aus dem Header des Requests genommen und durch JWT verifiziert.

// middleware/users.js

isLoggedIn: (req, res, next) => {
  try {
    const token = req.headers.authorization.split(' ')[1];
    const decoded = jwt.verify(
      token,
      'SECRETKEY'
    );
    req.userData = decoded;
    next();
  } catch (err) {
    return res.status(401).send({
      msg: 'Your session is not valid!'
    });
  }
}

In der markierten Zeile musst Du den gleichen Key angeben, mit dem Du den JWT bereits generiert hast.

Um nun eine Route zu schützen, bindest Du diese Middleware einfach beim Aufruf der Route wie folgt ein:

// routes/router.js

router.get('/secret-route', userMiddleware.isLoggedIn, (req, res, next) => {
  console.log(req.userData);
  res.send('This is the secret content. Only logged in users can see that!');
});

In req.userData stehen die Daten die wir im JWT Key abgelegt haben (in diesem Fall username und userId). Damit können wir z.B. bei geschützten Routen benutzerdefinierte Werte aus der Datenbank anhand der userId auslesen.

9. Fazit

➡️ Hier geht’s zum 2. Teil: Komplettes Login System mit Node.js & Vue.js | Vuex | Part [2/2]

That’s it! Im ersten Teil haben wir nun eine komplette RestAPI für die Validierung und das Session Handling für unsere Anwendung geschrieben. Dieses System kannst Du für Dein Frontend (egal, ob Angular, React oder Vue) verwenden. Danke für’s Lesen! 🙂

Beteilige dich an der Unterhaltung

1 Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.