Grundlagen der Computersicherheit: Authentifizierung und Autorisierung
Authentifizierung und Autorisierung bilden das Fundament der Computersicherheit. Sie verwenden Ihre Anmeldedaten, wie Benutzername und Passwort, um Ihre Identität zu bestätigen und sich als registrierter Nutzer zu identifizieren. Anschließend erhalten Sie Zugriff auf zusätzliche Rechte. Dieses Prinzip findet auch Anwendung, wenn Sie sich mit Ihrem Facebook- oder Google-Konto bei Online-Diensten anmelden.
In diesem Artikel zeigen wir, wie man eine Node.js-API mit JWT-Authentifizierung (JSON Web Tokens) entwickelt. Die hierfür verwendeten Werkzeuge sind:
- Express.js
- MongoDB-Datenbank
- Mongoose
- Dotenv
- Bcrypt.js
- Jsonwebtoken
Authentifizierung vs. Autorisierung
Was ist Authentifizierung?
Die Authentifizierung ist der Prozess der Benutzeridentifikation durch die Verwendung von Anmeldeinformationen, wie E-Mail, Passwort oder Token. Diese Anmeldeinformationen werden mit den gespeicherten Daten des registrierten Nutzers verglichen, die in lokalen Systemdateien oder in Datenbanken hinterlegt sind. Stimmen die angegebenen Daten mit den vorhandenen überein, ist die Authentifizierung erfolgreich und der Nutzer erhält Zugriff auf die gewünschten Ressourcen.
Was ist Autorisierung?
Die Autorisierung erfolgt nach der Authentifizierung. Sie setzt immer einen erfolgreichen Authentifizierungsprozess voraus. Sie bestimmt, ob ein authentifizierter Benutzer die Erlaubnis hat, auf bestimmte Ressourcen eines Systems oder einer Website zuzugreifen. In diesem Tutorial werden wir angemeldete Benutzer dazu berechtigen, ihre eigenen Daten abzurufen. Ein nicht angemeldeter Benutzer hat keinen Zugriff auf diese Informationen. Ein klassisches Beispiel für Autorisierung sind Social-Media-Plattformen. Ohne ein Konto ist der Zugriff auf die Inhalte nicht möglich. Ein weiteres Beispiel sind abonnementbasierte Inhalte. Auch wenn Sie sich auf einer Website anmelden, haben Sie keinen Zugriff auf die Inhalte, bis Sie das entsprechende Abonnement abgeschlossen haben.
Voraussetzungen
Für dieses Tutorial wird ein grundlegendes Verständnis von JavaScript, MongoDB und gute Kenntnisse in Node.js vorausgesetzt.
Bitte stellen Sie sicher, dass Node.js und npm auf Ihrem lokalen Rechner installiert sind. Sie können dies überprüfen, indem Sie in der Kommandozeile node -v
und npm -v
eingeben. Es sollten die jeweiligen Versionsnummern ausgegeben werden.
Ihre Versionsnummern können von meinen abweichen. NPM wird automatisch mit Node.js installiert. Falls Sie Node.js noch nicht installiert haben, können Sie es von der Node.js Webseite herunterladen.
Für das Schreiben des Codes benötigen Sie eine IDE (Integrierte Entwicklungsumgebung). In diesem Tutorial verwende ich VS Code. Sie können auch eine andere IDE Ihrer Wahl nutzen. Falls Sie noch keine IDE installiert haben, können Sie VS Code von der Visual Studio Webseite herunterladen und installieren. Wählen Sie die entsprechende Version für Ihr Betriebssystem.
Projekt-Setup
Erstellen Sie auf Ihrem lokalen Rechner einen Ordner namens `nodeapi` und öffnen Sie ihn in VS Code. Im VS Code Terminal initialisieren Sie den Node Package Manager mit folgendem Befehl:
npm init -y
Achten Sie darauf, dass Sie sich im Verzeichnis `nodeapi` befinden.
Dieser Befehl generiert eine `package.json`-Datei, die alle Projekt-Abhängigkeiten enthält.
Nun installieren wir die benötigten Pakete mit folgendem Befehl:
npm install express dotenv jsonwebtoken mongoose bcryptjs
Sie sollten nun eine Ordnerstruktur wie unten dargestellt haben.
Server erstellen und Datenbank verbinden
Erstellen Sie nun eine Datei namens `index.js` und einen Ordner namens `config`. Im Ordner `config` legen Sie zwei Dateien an: `conn.js` für die Datenbankverbindung und `config.env` für die Umgebungsvariblen. Schreiben Sie nun die folgenden Codes in die jeweiligen Dateien.
index.js
const express = require('express');
const dotenv = require('dotenv');
//Konfiguriere dotenv Dateien
dotenv.config({path:'./config/config.env'});
//Erstelle eine Express App
const app = express();
//Nutze express.json für JSON-Daten in Anfragen
app.use(express.json());
//Starte den Server
app.listen(process.env.PORT,()=>{
console.log(`Server lauscht auf Port ${process.env.PORT}`);
})
Es ist wichtig, dass Sie `dotenv` in der `index.js` Datei konfigurieren, bevor Sie andere Dateien aufrufen, die die Umgebungsvariblen verwenden.
conn.js
const mongoose = require('mongoose');
mongoose.connect(process.env.URI,
{ useNewUrlParser: true,
useUnifiedTopology: true })
.then((data) => {
console.log(`Datenbank verbunden mit ${data.connection.host}`)
})
config.env
URI = 'mongodb+srv://ghulamrabbani883:[email protected]/?retryWrites=true&w=majority'
PORT = 5000
Ich nutze hier eine MongoDB Atlas URI, Sie können aber auch einen localhost verwenden.
Erstellen von Modellen und Routen
Ein Modell definiert das Layout Ihrer Daten in der MongoDB und wird als JSON-Dokument gespeichert. Wir erstellen ein Modell mithilfe des Mongoose-Schemas.
Routing definiert, wie eine Anwendung auf Client-Anfragen reagiert. Für die Routenerstellung verwenden wir den Express-Router.
Routing-Methoden akzeptieren zwei Argumente: Die Route und die Callback-Funktion, die festlegt, was bei einer Client-Anfrage passieren soll. Optional kann ein drittes Argument als Middleware-Funktion hinzugefügt werden, wie wir es bei der Authentifizierung verwenden werden. Wir werden die Middleware nutzen, um Benutzer zu authentifizieren und zu autorisieren.
Erstellen Sie nun zwei Ordner: `routes` und `models`. Im `routes`-Ordner erstellen Sie die Datei `userRoute.js` und im `models`-Ordner die Datei `userModel.js`. Schreiben Sie die untenstehenden Codes in die jeweiligen Dateien.
userModel.js
const mongoose = require('mongoose');
//Erstelle Schema mit mongoose
const userSchema = new mongoose.Schema({
name: {
type:String,
required:true,
minLength:[4,'Name muss mindestens 4 Zeichen lang sein']
},
email:{
type:String,
required:true,
unique:true,
},
password:{
type:String,
required:true,
minLength:[8,'Passwort muss mindestens 8 Zeichen lang sein']
},
token:{
type:String
}
})
//Erstelle Modelle
const userModel = mongoose.model('user',userSchema);
module.exports = userModel;
userRoute.js
const express = require('express');
//Erstelle Express Router
const route = express.Router();
//Importiere userModel
const userModel = require('../models/userModel');
//Erstelle Registrierungsroute
route.post('/register',(req,res)=>{
})
//Erstelle Loginroute
route.post('/login',(req,res)=>{
})
//Erstelle Route um Benutzerdaten abzurufen
route.get('/user',(req,res)=>{
})
Implementierung der Routenfunktionalität und Erstellung von JWT-Tokens
Was ist JWT?
JSON Web Tokens (JWT) ist eine Javascript-Bibliothek zum Erstellen und Validieren von Tokens. JWT ist ein offener Standard für den Datenaustausch zwischen Client und Server. Wir verwenden zwei JWT-Funktionen: `sign`, um ein Token zu erstellen und `verify` um ein Token zu validieren.
Was ist bcrypt.js?
Bcrypt.js ist eine Hash-Funktion, die von Niels Provos und David Mazières entwickelt wurde. Sie verwendet einen Hash-Algorithmus zur Verschlüsselung von Passwörtern. Wir werden zwei Funktionen nutzen: `hash`, um Hash-Werte zu erstellen und `compare`, um Passwörter zu vergleichen.
Implementierung der Routenfunktionalität
Die Callback-Funktion beim Routing benötigt drei Argumente: `request`, `response` und die `next`-Funktion. Das `next`-Argument ist optional und wird nur benötigt, wenn Sie eine weitere Callback-Funktion oder Middleware aufrufen möchten. Die Argumente müssen in der Reihenfolge `request`, `response` und `next` übergeben werden. Aktualisieren Sie nun die Dateien `userRoute.js`, `config.env` und `index.js` mit den folgenden Codes.
userRoute.js
//Importiere benötigte Dateien und Bibliotheken
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
//Erstelle Express Router
const route = express.Router();
//Importiere userModel
const userModel = require('../models/userModel');
//Erstelle Registrierungsroute
route.post("/register", async (req, res) => {
try {
const { name, email, password } = req.body;
//Prüfe ob alle Daten vorhanden sind
if (!name || !email || !password) {
return res.json({ message: 'Bitte alle Daten eingeben' })
}
//Prüfe ob Benutzer existiert
const userExist = await userModel.findOne({ email: req.body.email });
if (userExist) {
return res.json({ message: 'Benutzer existiert bereits mit dieser E-Mail-Adresse' })
}
//Hash das Passwort
const salt = await bcrypt.genSalt(10);
const hashPassword = await bcrypt.hash(req.body.password, salt);
req.body.password = hashPassword;
const user = new userModel(req.body);
await user.save();
const token = await jwt.sign({ id: user._id }, process.env.SECRET_KEY, {
expiresIn: process.env.JWT_EXPIRE,
});
return res.cookie({ 'token': token }).json({ success: true, message: 'Benutzer erfolgreich registriert', data: user })
} catch (error) {
return res.json({ error: error });
}
})
//Erstelle Login Route
route.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
//Prüfe ob alle Daten vorhanden sind
if (!email || !password) {
return res.json({ message: 'Bitte alle Daten eingeben' })
}
//Prüfe ob Benutzer existiert
const userExist = await userModel.findOne({email:req.body.email});
if(!userExist){
return res.json({message:'Falsche Anmeldedaten'})
}
//Prüfe ob Passwort übereinstimmt
const isPasswordMatched = await bcrypt.compare(password,userExist.password);
if(!isPasswordMatched){
return res.json({message:'Falsches Passwort'});
}
const token = await jwt.sign({ id: userExist._id }, process.env.SECRET_KEY, {
expiresIn: process.env.JWT_EXPIRE,
});
return res.cookie({"token":token}).json({success:true,message:'Erfolgreich angemeldet'})
} catch (error) {
return res.json({ error: error });
}
})
//Erstelle Route um Benutzerdaten abzurufen
route.get('/user', async (req, res) => {
try {
const user = await userModel.find();
if(!user){
return res.json({message:'Keine Benutzer gefunden'})
}
return res.json({user:user})
} catch (error) {
return res.json({ error: error });
}
})
module.exports = route;
Bei der Verwendung von `async`-Funktionen ist ein `try-catch`-Block erforderlich, um unbehandelte Promise-Rejection-Fehler zu vermeiden.
config.env
URI = 'mongodb+srv://ghulamrabbani883:[email protected]/?retryWrites=true&w=majority'
PORT = 5000
SECRET_KEY = KGGK>HKHVHJVKBKJKJBKBKHKBMKHB
JWT_EXPIRE = 2d
index.js
const express = require('express');
const dotenv = require('dotenv');
//Konfiguriere dotenv Dateien
dotenv.config({path:'./config/config.env'});
require('./config/conn');
//Erstelle eine Express App
const app = express();
const route = require('./routes/userRoute');
//Nutze express.json für JSON-Daten in Anfragen
app.use(express.json());
//Nutze Routen
app.use('/api', route);
//Starte den Server
app.listen(process.env.PORT,()=>{
console.log(`Server lauscht auf Port ${process.env.PORT}`);
})
Erstellen von Middleware zur Authentifizierung von Benutzern
Was ist Middleware?
Middleware ist eine Funktion, die Zugriff auf das Anfrage- und Antwortobjekt sowie die `next`-Funktion im Anfrage-Antwort-Zyklus hat. Die `next`-Funktion wird aufgerufen, wenn die Ausführung der aktuellen Funktion beendet ist. Wie bereits erwähnt, verwenden Sie `next()`, wenn Sie eine weitere Callback- oder Middleware-Funktion aufrufen möchten.
Erstellen Sie einen neuen Ordner namens `middleware` und darin eine Datei `auth.js`. Schreiben Sie folgenden Code in diese Datei.
auth.js
const userModel = require('../models/userModel');
const jwt = require('jsonwebtoken');
const isAuthenticated = async (req,res,next)=>{
try {
const {token} = req.cookies;
if(!token){
return next('Bitte anmelden, um auf die Daten zuzugreifen');
}
const verify = await jwt.verify(token,process.env.SECRET_KEY);
req.user = await userModel.findById(verify.id);
next();
} catch (error) {
return next(error);
}
}
module.exports = isAuthenticated;
Installieren Sie nun die `cookie-parser`-Bibliothek, um den Cookie-Parser in Ihrer App zu konfigurieren. Der Cookie-Parser hilft Ihnen, auf die im Cookie gespeicherten Tokens zuzugreifen. Ohne den Cookie-Parser können Sie nicht auf Cookies im Anforderungsheader zugreifen. Installieren Sie den Cookie-Parser wie folgt:
npm i cookie-parser
Sie haben den Cookie-Parser nun installiert. Konfigurieren Sie Ihre App durch Modifikation der `index.js` und fügen Sie Middleware zur `/user`-Route hinzu.
index.js
const cookieParser = require('cookie-parser');
const express = require('express');
const dotenv = require('dotenv');
//Konfiguriere dotenv Dateien
dotenv.config({path:'./config/config.env'});
require('./config/conn');
//Erstelle eine Express App
const app = express();
const route = require('./routes/userRoute');
//Nutze express.json für JSON-Daten in Anfragen
app.use(express.json());
//Konfiguriere cookie-parser
app.use(cookieParser());
//Nutze Routen
app.use('/api', route);
//Starte den Server
app.listen(process.env.PORT,()=>{
console.log(`Server lauscht auf Port ${process.env.PORT}`);
})
userRoute.js
//Importiere benötigte Dateien und Bibliotheken
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const isAuthenticated = require('../middleware/auth');
//Erstelle Express Router
const route = express.Router();
//Importiere userModel
const userModel = require('../models/userModel');
//Erstelle Route um Benutzerdaten abzurufen
route.get('/user', isAuthenticated, async (req, res) => {
try {
const user = await userModel.find();
if (!user) {
return res.json({ message: 'Keine Benutzer gefunden' })
}
return res.json({ user: user })
} catch (error) {
return res.json({ error: error });
}
})
module.exports = route;
Die `/user`-Route ist nun nur noch für angemeldete Benutzer zugänglich.
Testen der APIs mit Postman
Vor dem Testen der APIs müssen Sie die `package.json` Datei aktualisieren, indem Sie folgende Zeilen hinzufügen.
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"start": "node index.js",
"dev": "nodemon index.js"
},
Der Server kann mit `npm start` gestartet werden, dies führt jedoch nur eine einmalige Ausführung aus. Für die Live-Aktualisierung benötigen Sie `nodemon`. Installieren Sie es global mit:
npm install -g nodemon
Um den Server zu starten, geben Sie im Terminal `npm run dev` ein. Sie sollten die folgende Ausgabe erhalten.
Nun sollte Ihr Code fertig und der Server betriebsbereit sein. Gehen Sie zu Postman um die Funktionalität zu testen.
Was ist Postman?
Postman ist eine Software zur Erstellung, Entwicklung und zum Testen von APIs.
Wenn Sie Postman noch nicht heruntergeladen haben, tun Sie dies von der Postman Website.
Erstellen Sie eine Collection namens `nodeAPItest` und erstellen Sie darin drei Anfragen: `Registrieren`, `Anmelden` und `Benutzer`. Sie sollten folgende Dateien haben.
Senden Sie JSON-Daten an `localhost:5000/api/register`. Sie sollten das folgende Ergebnis erhalten.
Da wir beim Registrieren auch ein Token erstellen und in den Cookies speichern, erhalten Sie bei einer Anfrage an `localhost:5000/api/user` die Benutzerdetails. Die restlichen Anfragen können Sie ebenfalls in Postman testen.
Den vollständigen Code finden Sie auf meinem Github-Konto.
Fazit
In diesem Tutorial haben wir gelernt, wie man Authentifizierung mit JWT-Tokens in einer NodeJS API einbindet und Benutzer autorisiert um auf die Benutzerdaten zuzugreifen.
Viel Spaß beim Programmieren!