Double Opt-In in Node.js: Bestätigungslinks versenden

node-double-opt-in

Das Double Opt-In Verfahren hat sich in den letzten Jahren als Standard für jegliche Art von Anmeldungen im Internet etabliert. Was sich genau dahinter verbirgt und wie Du Double Opt-In in Deine Node.js Anwendung einbauen kannst, zeige ich Dir hier.

Beim Double Opt-In Verfahren wird neben der Anmeldungen in einem Formular, z.B. auf einer Website noch zusätzlich ein Aktivierungs-Link per E-Mail versendet und erst nach dessen Bestätigung ist der Anmeldeprozess beendet, bzw. die Anmeldung verifiziert.

Klassische Anwendungsgebiete sind beispielsweise:

  • Newsletter An-/Abmeldungen,
  • Gewinnspiel Anmeldungen, oder
  • Login Systeme

In diesem Tutorial bauen wir ein Double Opt-In Verfahren in eine bestehende RestAPI eines Node.js Login Systems ein. Dazu habe ich bereits in einem anderen Tutorial gezeigt, wie wir eine RestAPI mit Node.js erstellen und an unser Frontend, z.B. Vue.js anbinden können.

Tutorial Node.js Login System

Solltest Du bereits ein fertiges System haben und nur das Double Opt-In Verfahren erweitern wollen, kannst Du getrost weiterlesen. Ansonsten empfehle ich Dir zu erst das verlinkte Tutorial anzuschauen.

Falls Du noch nicht mit Node.js vertraut bist, kannst Du Dir zu erst die Grundlagen zu Node.js anschauen.

1. Vorteile von Double-Opt In

Neben dem normalen Single Opt-In Verfahren, bei dem man sich einfach für eine Sache anmeldet – ohne weitere Bestätigung – gibt es auch das bessere Double Opt-In Verfahren. Das folgende Schaubild soll den Unterschied im Registrierungsprozess erläutern:

Single Opt-In vs. Double Opt-In
Single Opt-In vs. Double Opt-In

Das Double Opt-In hat viele Vorteile und ist mittlerweile etablierter Standard bei jeglichen Webanwendungen, Apps, etc.

Der wohl wichtigste Fakt ist, dass alle bestätigen Anmeldungen echt (verifiziert) sind. Bedeutet, dass sich zwar Bots im Formular eintragen können, jedoch wird der entsprechende Account niemals verifiziert, da die Bots keine Verifizierungs-Links in den Bestätigungsmails anklicken (können).

Weiterhin ist es aus datenschutz-rechtlicher Sicht wahrscheinlich von Vorteil, da der Nutzer hier nochmal explizit einer Anmeldung zustimmt. Gerade in EU-Ländern kann es in manchen Fällen von der DSGVO gefordert sein.

Aber Achtung: Ich kann keine fachliche Meinung zu juristischen Fragen abgeben!

Nun beginnen wir direkt und verschicken mal ein paar Bestätigungsmails (mit Hilfe vom nodemailer) mit Aktivierungs-Links, viel Spaß! 🙂

2. Abhängigkeiten installieren

Im ersten Schritt beginnen wir also damit uns installieren die benötigten Packages:

PackageBeschreibung
nodemailerZum Versenden der Bestätigungs-Emails
email-validatorValidiert eingegebene E-Mail Adressen
Benötigte npm Packages

Diese Packages können wir über folgenden Befehl installieren:

npm install nodemailer email-validator

3. MySQL Datenbank Struktur anpassen

Unsere bestehende users Tabelle erweitern wir nun um drei weitere Spalten:

SpaltennameVerwendungszweck
email (varchar)E-Mail Adresse des Nutzers
token (varchar)Einmaliger Token zum Generieren der Aktivierungs-Links
active (int)Status des Eintrags (0 = nicht verifiziert, 1 = verifiziert)
Erweiterung der MySQL Tabelle

Die komplette MySQL Tabelle sieht jetzt wie folgt aus:

phpMyAdmin Ansicht der erweiterten „users“ Tabelle
phpMyAdmin Ansicht der erweiterten „users“ Tabelle

Hier nochmal der Hinweis: Dieses Tutorial baut auf einem anderen Tutorial von mir auf: Node.js Login System mit Express, JWT und MySQL (RestAPI)

4. Registrierung (/sign-up) und Login (/login) Routen anpassen

Um die neuen Spalten zu füllen, passen wir unsere bestehende Registrierungs-Route (/sign-up) und Login-Route (/route) ein wenig an.

Dazu fügen wir die hervorgehobenen Zeilen hinzu bzw. passen sie entsprechend an.

Geändert wird hier das SQL-Query, damit unsere neuen Werte, also die E-Mail Adresse, der Token und der Account-Status (active) in die Datenbank eingetragen werden. Als Token verwenden wir wie auch für die Benutzer-ID einfach eine einmalige ID vom uuid Package.

Ab Zeile 40 rufen wir dann die Funktion sendOptInMail() vom mailer auf. Die Funktion implementieren wir gleich in Schritt 8.

// 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
		)}) OR LOWER(email) = LOWER(${db.escape(req.body.email)});`,
		(err, result) => {
			if (result.length) {
				return res.status(409).send({
					msg: "This username or email 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 {
						let email = req.body.email;
						let userID = uuid.v4();
						let token = uuid.v4();

						// has hashed pw => add to database
						db.query(
							`INSERT INTO users (id, username, email, password, registered, active, token) VALUES ('${userID}', ${db.escape(
								req.body.username
							)}, '${email}', ${db.escape(
								hash
							)}, now(), 0, '${token}')`,
							async (err, result) => {
								if (err) {
									throw err;
									return res.status(400).send({
										msg: err,
									});
								}

								await mailer.sendOptInMail(
									email,
									userID,
									token
								);

								return res.status(201).send({
									msg: "Registered!",
								});
							}
						);
					}
				});
			}
		}
	);
});

Damit wir sendOptInMail() aufrufen können, müssen wir mailer einbinden. Dazu kannst Du die Datei einfach schon mal anlegen, Inhalt kommt – wie gesagt – gleich.

// routes/router.js

[...]
const mailer = require("../lib/mailer.js");
[...]

5. E-Mail Adresse validieren

Um die eingegebene E-Mail Adresse zu überprüfen, erweitern wir unsere Middleware in der users.js um die hervorgehobenen Zeilen.

Dort wird geprüft, ob im Body unseres Requests ein Wert mit dem Namen email übergeben wurde und ob es sich dabei um eine gültige E-Mail Adresse handelt. Falls nicht, wird eine Fehlermeldung zurückgegeben. Ansonsten wird am Ende mit next() die Route weiter ausgeführt.

Zur Validierung der E-Mail nutzen wir das email-validator Package. Das kannst Du aber auch über einen eigenen regulären Ausdruck machen, zur Einfachheit verwenden wir hier aber ein externes Modul.

// middleware/users.js

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",
			});
		}

		// valide email
		if (!req.body.email || !validator.validate(req.body.email)) {
			return res.status(400).send({
				msg: "Please enter a valid email address",
			});
		}

		// 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();
	},

6. Account Aktivierung beim Login überprüfen

In der /login Route kannst Du diese Abfrage noch einbauen, damit man sich nicht anmelden kann, falls der Account noch nicht bestätigt wurde. Bei mir sitzt die Abfrage nach der Überprüfung, ob ein entsprechender Datenbank Eintrag existiert. Ab Zeile 20 der /login Route, falls Du Dich an meinem anderen Tutorial orientieren willst.

// routes/router.js

[...]
if (!result[0]["active"]) {
  return res.status(401).send({
    msg: "Your account is not activated!",
  });
}
[...]

Falls Du das System bereits um eigene Funktion erweitert hast, solltest Du diese Abfrage noch an anderen relevanten Stellen einbauen, z.B. in einer Passwort-Vergessen-Funktion.

7. Verifizierungs Route erstellen (/verify)

Mit der Verifizierungs Route können wir einen registrierten Account aktivieren. Den Link dazu verschicken wir dann im letzten Schritt per Mail. Der Link besteht aus der Benutzer-ID und dem einmaligen Token.

Dazu legen wir eine neue Route an, die uns den Benutzer anhand der Benutzer-ID aus der Datenbank ausliest. Zuerst prüfen wir, ob der Eintrag überhaupt existiert und geben ggf. eine Fehlermeldung zurück.

Ab Zeile 25 wird geprüft, ob der Account bereits aktiviert ist und ab Zeile 32 prüfen wir den Token ab.

Sollten alle Überprüfungen korrekt sein, setzen wir den Benutzer-Account aktiv (ab Zeile 39) und geben eine Erfolgsmeldung mit dem Statuscode 200 zurück.

// routes/router.js

router.get("/verify/:userID/:token", (req, res, next) => {
	let userID = req.params.userID;
	let token = req.params.token;

	db.query(
		`SELECT * FROM users WHERE id = ${db.escape(userID)}`,
		(err, result) => {
			// user does not exists
			if (err) {
				throw err;
				return res.status(400).send({
					msg: err,
				});
			}

			// no result from database
			if (!result.length) {
				return res.status(409).send({
					msg: "The requested parameters are incorrect!",
				});
			}

			// already activated
			if (result[0]["active"]) {
				return res.status(409).send({
					msg: "Account is already activated!",
				});
			}

			// wrong activation token
			if (result[0]["token"] !== token) {
				return res.status(401).send({
					msg: "The requested parameters are incorrect!",
				});
			}

			// set account active
			db.query(
				`UPDATE users SET active = 1 WHERE id = '${userID}'`,
				(err, result) => {
					if (err) {
						throw err;
						return res.status(400).send({
							msg: err,
						});
					}

					return res.status(200).send({
						msg: "Account activated",
					});
				}
			);
		}
	);
});

8. Bestätigungsmail versenden

Die Bestätigungsmail enthält einen Verifizierungs-Link, bei dessen Aufruf der Account bestätigt, also aktiviert werden soll. Den Link bauen wir uns selber zusammen und versenden ihn dann mit Hilfe des nodemailer Packages.

Dazu können wir im Ordner lib eine mailer.js anlegen und den nodemailer einbinden.

Nun stellen wir den nodemailer so ein, dass er Mails über unseren Mailserver versenden kann. Das passiert mit Hilfe der createTransport() Funktion von Zeile 5 bis 16.

Die Zugangsdaten zum Mailserver solltest Du als Umgebungsvariablen von Node.js deklarieren (process.env.*), damit keine sensiblen Daten direkt im Quellcode stehen und die Daten einfach auf unterschiedlichen Entwicklungsständen (development & production) anpassen kannst.

// lib/mailer.js

const nodemailer = require("nodemailer");

let transporter = nodemailer.createTransport({
	host: process.env.MAIL_HOST,
	port: 465,
	secure: true,
	auth: {
		user: process.env.MAIL_USER,
		pass: process.env.MAIL_PASSWORD,
	},
	tls: {
		rejectUnauthorized: false,
	},
});

Nun erstellen wir die asynchrone Funktion sendOptInMail(). Den Aktivierungs-Link bauen wir uns aus der Benutzer-ID und dem Token zusammen, so wie wir es in der /verify Route definiert haben.

Für die E-Mail geben wir einige Parameter an:

  • from: Absenderadresse
  • to: Empfängeradresse
  • subject: Betreff
  • text: Textinhalt der E-Mail (falls der Mail Client kein HTML unterstützt)
  • html: HTML-Inhalt der E-Mail

Das wichtigste ist, dass wir den Aktivierungs-Link in der E-Mail einbetten, so dass der Benutzer darüber seinen Account aktivieren kann.

// lib/mailer.js

module.exports = {
	async sendOptInMail(email, userID, token) {
		let activationLink = `${process.env.BASE_URL}api/verify/${userID}/${token}`;

		let mail = {
			from: process.env.SENDER_MAIL,
			to: email,
			subject: "Please active your account",
			text: `To activate your account, please click this link: ${activationLink}`,
			html: `<p>To activate your account, please click this link: <a href="${activationLink}">${activationLink}</a></p>`,
		};

		await transporter.sendMail(mail);
	},
};

Am Ende wird die konfigurierte E-Mail über transporter.sendMail(mail); versendet.

9. Double Opt-In testen

Alles fertig! Jetzt kannst Du Deine RestAPI testen (Anleitung gibt es hier). Probiere auch aus, ob Du dich erneut nicht registrieren kannst, ob Du Dich nicht anmelden kannst, wenn der Account nicht bestätigt wurde und ob der Bestätigungslink funktioniert.

Wenn alles klappt: Glückwunsch & weiterhin happy coding! 🙂

Ähnliche Beiträge
Beteilige dich an der Unterhaltung

4 Kommentare

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

bold italic underline strikeThrough
insertOrderedList insertUnorderedList outdent indent
removeFormat
createLink unlink
code

Das könnte dich auch interessieren