
Vertrauenswürdige Gruppenauthentifizierung mit SMS und Express
Lesedauer: 11 Minuten
Einführung
Allein an einem Projekt zu arbeiten, kann manchmal mühsam und einsam sein. Deshalb kann die Zusammenarbeit mit Freunden den Prozess viel angenehmer machen! Allerdings kann es mühsam sein, Treffen zu arrangieren und geeignete Zeiten für alle zu finden. Zum Glück gibt es eine Lösung - die Entwicklung einer Webanwendung, mit der Sie nahtlos online zusammenarbeiten können.
Dieses Tutorial führt Sie durch die Erstellung eines einfachen Authentifizierungsablaufs mit der Vonage Verify API. Dadurch können Ihre Freunde sicher auf die kollaborative App zugreifen, ohne sich mit komplexen Passwortrichtlinien oder Verschlüsselung auseinandersetzen zu müssen. Stattdessen nutzen Sie ihre Telefonnummern und das Verifizierungssystem von Vonage, um sie zu authentifizieren und sie über Sitzungscookies angemeldet zu halten.
tl;drI Wenn Sie die Anwendung sofort einsetzen möchten, finden Sie den gesamten des notwendigen Codes auf GitHub.
Voraussetzungen
Bevor wir loslegen, sollten Sie sich vergewissern, dass Sie alles haben:
Node.js und npm installiert
Grundlegende Vertrautheit mit Express.js und SQLite
Eine virtuelle Vonage-Nummer mit SMS-Funktionen
Vonage API-Konto
Um dieses Tutorial durchzuführen, benötigen Sie ein Vonage API-Konto. Wenn Sie noch keines haben, können Sie sich noch heute anmelden und mit einem kostenlosen Guthaben beginnen. Sobald Sie ein Konto haben, finden Sie Ihren API-Schlüssel und Ihr API-Geheimnis oben auf dem Vonage-API-Dashboard.
In diesem Lernprogramm wird auch eine virtuelle Telefonnummer verwendet. Um eine zu erwerben, gehen Sie zu Rufnummern > Rufnummern kaufen und suchen Sie nach einer Nummer, die Ihren Anforderungen entspricht.
Optional können Sie ein ein Werkzeug wie ngrok verwenden, um Ihren lokalen Entwicklungsserver während der Tests öffentlich zugänglich zu machen.
Einrichten
Erstellen Sie zunächst ein neues Node.js-Projekt und installieren Sie die erforderlichen Pakete:
Dadurch werden Express, SQLite3-Datenbankunterstützung, Middleware für Sitzungen/Cookies und die Vonage API-Client-Bibliothek hinzugefügt.
Als nächstes erstellen Sie eine .env Datei und fügen Sie Ihre Vonage-Anmeldedaten und Ihre virtuelle Nummer hinzu:
Außerdem müssen Sie einen geheimen Schlüssel für die Speicherung der Sitzung generieren:
Legen Sie alle anderen Umgebungsvariablen fest, die Sie benötigen, z. B. einen Einladungscode, eine Projektdomäne, eine Vonage-App-ID usw.
Express konfigurieren
In Ihrer Hauptserverdatei (z. B., app.js), benötigen Sie Express und richten den Basisserver ein:
const express = require('express');
const app = express();
const port = 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
Als Nächstes fügen Sie Middleware hinzu, um JSON-Anfragen zu parsen, Sitzungen zu verwalten und die SQLite3-Sitzungsspeicherung zu aktivieren:
// parse client requests in JSON
app.use(express.json());
// install packages to do session management
const session = require('express-session');
const SQLiteStore = require('connect-sqlite3')(session);
// configure automatic session storage in SQLite db
app.use(require('cookie-parser')());
app.use(session({
store: new SQLiteStore,
secret: process.env.SESH_SECRET || 'your-secret-key-here', // Provide a secret option
resave: false,
saveUninitialized: false,
cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 } // 1 week
}));Vergessen Sie nicht, das Vonage Server SDK-Objekt mit Ihren API-Anmeldedaten zu initialisieren:
const Vonage = require('@vonage/server-sdk');
const vonage = new Vonage({
apiKey: process.env.API_KEY,
apiSecret: process.env.API_SECRET,
applicationId: process.env.APP_ID,
privateKey: process.env.PRIVATE_KEY_PATH,
}); Einrichten der SQLite-Datenbank
Sie benötigen einige SQLite-Tabellen, um Benutzerdaten und Sitzungen während des Authentifizierungsvorgangs zu speichern:
const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database(':memory:');
db.serialize(function(){
if (!exists) {
db.run('CREATE TABLE Sessions (phone NUMERIC, id TEXT)');
db.run('CREATE TABLE allowlist (phone NUMERIC, username TEXT)');
db.run('CREATE TABLE Authors (username TEXT)');
}
});Die Tabelle Sessions Tabelle enthält vorübergehend die Telefonnummern der Benutzer und die IDs der Vonage-Anfragen während der Überprüfung. Die Allowlist verfolgt erlaubte Anmeldungen, optional mit einem vorab genehmigten Benutzernamen. Authors ist die persistierte Liste der authentifizierten Benutzernamen.
Handle Routen
Routen für Ihre Ansichten
Ihr Server hat bereits eine Route für die Hauptseite unter /. Darunter können Sie zwei weitere Routen für eine Anmelde- oder Login-Seite für Ihre Nutzer und eine Verwaltungsseite für Sie einrichten:
// View Routes
app.get('/', function(request, response) {
response.sendFile(__dirname + '/views/index.html');
});
app.get('/signup', function(request, response) {
response.sendFile(__dirname + '/views/signup.html');
});
app.get('/admin', function(request, response) {
if (isAdmin(request.session)) {
response.sendFile(__dirname + '/views/admin.html');
} else {
response.sendFile(__dirname + '/views/index.html');
}
});Um mit der Verwaltung des Zugriffs auf Ihre Verwaltungsfunktionen zu beginnen, müssen Sie auch die isAdmin Funktion, auf die in Ihrer /admin Route verweist. Sie teilt Ihre Liste der Admins aus .env in ein Array auf und sucht nach einer genauen Übereinstimmung mit dem Benutzernamen in der aktuellen Sitzung:
function isAdmin(sesh) {
let admins = process.env.ADMINS.split(',');
return admins.includes(sesh.username);
} Der Verwaltungsendpunkt
Der erste Schritt im Arbeitsablauf des Hinzufügens eines Benutzers besteht darin, dass der Administrator die Telefonnummer zu einer Erlaubnisliste hinzufügt. Da sich dieselbe Person möglicherweise von verschiedenen Geräten aus anmelden möchte (wodurch zusätzliche Sitzungscookies erforderlich werden) oder ihre Sitzung abläuft, kann der Administrator die Telefonnummer optional mit einem bestehenden Benutzernamen verknüpfen.
Um zu beginnen, deklarieren Sie den Endpunkt unter /invite und fügen Sie eine Sicherheitsüberprüfung hinzu, um sicherzustellen, dass diese Person noch ein Administrator ist:
app.post('/invite', function(request, response) {
if (!isAdmin(request.session)) {
response.status(500).send({message: "Sorry, you're not an admin"});
return;
}
});Wenn die Person, die versucht, eine Einladung hinzuzufügen, kein Administrator ist, sollte die Anfrage fehlschlagen.
Als nächstes holt die Funktion die Telefonnummer aus der Anfrage. Nach einer leichten Gültigkeitsprüfung wird sie in die Erlaubnisliste aufgenommen. Wenn der Administrator einen Benutzernamen angegeben hat, wird auch dieser hinzugefügt:
app.post('/invite', function(request, response) {
if (!isAdmin(request.session)) {
...
}
let phone = request.body.phone;
if (!isNaN(phone)) {
if (request.body.username) {
db.run('INSERT INTO Allowlist (phone, username) VALUES ($phone, $user)', {
$phone: phone,
$user: request.body.username
});
} else {
db.run('INSERT INTO Allowlist (phone) VALUES ($phone)', {
$phone: phone
});
}
}
});Sobald die neue Telefonnummer zur Liste der zulässigen Nummern hinzugefügt wurde, müssen Sie dem neuen Benutzer eine Einladung schicken. Der Text wird von der Telefonnummer gesendet, die Sie in .envgespeichert haben, und der Benutzer erhält den aktuellen Einladungscode, den er zurückschicken kann.
Sie können diesen Schritt auch ganz überspringen und die Verifizierungs-PIN senden. Auf diese Weise können Sie jedoch alle kontextbezogenen Informationen bereitstellen, die für den Benutzer von Nutzen sein könnten, wie z. B. die Anmelde-URL. Da die Verify-PINs von Vonage nur fünf Minuten gültig sind, können Sie so auch sicherstellen, dass die PIN des Empfängers nicht abläuft, bevor er sie sieht:
app.post('/invite', function(request, response) {
if (!isAdmin(request.session)) {
...
}
let phone = request.body.phone;
if (!isNaN(phone)) {
if (request.body.username) {
...
} else {
...
}
vonage.messages.send(
new SMS(
`Please reply to this message with "${process.env.INVITE_CODE}" to get your PIN.`,
phone,
process.env.APP_NUM,
),
);
}
}); Der Webhook-Endpunkt
Bevor Sie eingehende Texte mit der Vonage Messages API empfangen können, müssen wir unsere Anwendungseinstellungen im Vonage Dashboard konfigurieren. Wir kaufen eine virtuelle Nummer und setzen die Inbound Webhook URL unter der Konfiguration der Nummer auf unsere lokale ngrok URL, die unseren Node.js Server offenlegt.
Zum Beispiel können wir die Inbound Webhook URL auf etwas wie http://1234abc.ngrok.io/answer um eingehende Nachrichten an unseren lokalen /answer-Endpunkt zu senden.
Nachdem Sie Ihre Telefonnummer konfiguriert haben, können Sie die Logik für den Webhook-Endpunkt hinzufügen. Sie überprüfen, ob der Text den aktuellen Einladungscode enthält und ob die Telefonnummer, von der er stammt, in der Zulässigkeitsliste enthalten ist. Wenn diese Bedingungen erfüllt sind, senden Sie eine Anfrage zur Überprüfung und speichern die Telefonnummer und die ID, die Sie als Antwort erhalten, in Ihrer Sitzungsdatenbank:
app.post('/answer', function(request, response) {
let from = request.body.from;
if (request.body.text === process.env.INVITE_CODE) {
db.all('SELECT * from Allowlist WHERE phone = $from',
{$from: from},
function(err, rows) {
if (rows.length) {
vonage.verify.request({
number: from,
brand: process.env.PROJECT_DOMAIN
}, (err, result) => {
db.run('INSERT INTO Sessions (phone, id) VALUES ($phone, $id)', {
$phone: from,
$id: result.request_id
});
response.status(204).end();
});
}
});
}
});
Dieses Mal erhält der neue Benutzer einen von Vonage Verify automatisch generierten Text mit seiner PIN. Sie haben die Telefonnummer Ihrer Anwendung und die Domain auf ngrok angegeben, um sich zu identifizieren, aber ansonsten ist der Text ein Standardtext. Der Benutzer muss in dieser Nachricht etwas finden, worauf er reagieren kann. Mit einem gespeicherten Datensatz warten wir darauf, dass der Benutzer den letzten Schritt über die Webanwendung ausführt.
Der Anmelde- oder Login-Endpunkt
Der neue Benutzer sendet seine Telefonnummer, seinen Benutzernamen und seine PIN über den Web-Client. Nur der Benutzername wird gespeichert. Die anderen Werte dienen dem Authentifizierungsprozess, und wenn die Anmeldung erfolgreich war, werden sie aus dem Datenspeicher entfernt.
Fügen Sie einen neuen /login Endpunkt zu Ihrem Server hinzu, und führen Sie als ersten Schritt eine schnelle Validierung des Benutzernamens durch. Das Beispiel hier lässt nur einfache Zeichen zu, was für Ihre Zwecke in Ordnung sein könnte, oder Sie möchten vielleicht einen robusteren Satz von Optionen. Nach dieser Überprüfung finden Sie die Sitzung für die angegebene Telefonnummer:
app.post('/login', function(request, response) {
let allowed = RegExp('[A-Za-z0-9_-]+');
let username = request.body.username;
if (!allowed.test(username)) {
return;
}
db.each('SELECT * FROM Sessions WHERE phone = $phone', {
$phone: request.body.phone
}, function(error, sesh) {
});
});Innerhalb des Rückrufs, der die Sitzungszeile bereitstellt, wird eine weitere Überprüfung des Benutzernamens durchgeführt: Diesmal wird festgestellt, ob er bereits verwendet wird und, falls ja, ob diese Telefonnummer berechtigt ist, sich mit ihm anzumelden:
app.post('/login', function(request, response) {
...
db.each('SELECT * FROM Sessions WHERE phone = $phone',{
$phone: request.body.phone
}, function(error, sesh) {
let broken = false;
db.all('SELECT * FROM Authors WHERE username = $user', {
$user: username
}, function(err, rows) {
if (rows.length) {
db.all('SELECT * FROM Allowlist WHERE username = $user AND phone = $phone', {
$user: username,
$phone: sesh.phone
}, function(e, r) {
if (e || !r.length) broken = true;
});
}
});
if (!broken) {
}
});
});Wenn alle Überprüfungen des Benutzernamens erfolgreich waren, verwenden Sie ein Flag, um zu bestätigen, dass Sie fortfahren können und dass die vom Client erhaltene PIN für diese Überprüfungsanfrage korrekt ist. Wenn dies der Fall ist, erhalten Sie in der Antwort den Status 0 und können die Sitzung und die Datensätze der Zulassungsliste, die Sie während dieses Prozesses verwendet haben, sicher löschen. Der letzte Schritt besteht darin, den Benutzernamen in die Sitzung einzutragen:
app.post('/login', function (request, response) {
let allowed = RegExp('[A-Za-z0-9_-]+');
let username = request.body.username;
if (!allowed.test(username)) {
response.status(500).send({ message: 'Please use basic characters for your username' });
return;
}
db.each('SELECT * FROM Sessions WHERE phone = $phone', {
$phone: request.body.phone
}, function (error, sesh) {
db.all('SELECT * FROM Authors WHERE username = $user', { $user: username }, function (err, rows) {
if (rows.length) {
db.all('SELECT * FROM Allowlist WHERE username = $user AND phone = $phone', {
$user: username,
$phone: sesh.phone
}, function (e, r) {
if (e || !r.length) {
response.status(500).send({ message: 'Please choose a different username' });
return;
}
});
}
vonage.verify.check(sesh.id, request.body.pin)
.then(result => {
if (result && result.status === '0') {
db.serialize(function () {
db.run('INSERT INTO Authors (username) VALUES ($user)', {
$user: username
});
db.run('DELETE FROM Allowlist WHERE phone = $phone', {
$phone: sesh.phone
});
db.run('DELETE FROM Sessions WHERE phone = $phone', {
$phone: sesh.phone
});
});
request.session.username = username;
response.status(200).send({ message: "Success" });
}
})
.catch(err => {
// handle errors
console.error(err);
if (err) {
console.log('Error occurred:', err);
response.status(500).send({ message: 'Error verifying your info' });
}
});
});
});
});
Einige Markierungen hinzufügen
Um Daten auf der Client-Seite zu sammeln, benötigen Sie zwei ähnliche Formulare: ein Verwaltungsformular und ein Anmeldeformular. Das Verwaltungsformular wird Einladungen an neue Benutzer auslösen, und das Anmeldeformular wird neue Sitzungen erstellen. Die Seite index.html ist die Landing Page für Ihr Projekt, auf der Sie alle gewünschten Informationen und Funktionen bereitstellen können. Es kann jedoch nützlich sein, ihren Inhalt in admin.html und signup.html zu kopieren, damit Sie Ihr Gerüst haben.
Innerhalb des <main> Tag in admin.html ersetzen Sie den HTML-Code durch ein einfaches Formular zur Erfassung einer Telefonnummer und eines Benutzernamens:
<main>
<h2>
Invite people to your application
</h2>
<form action="/invite" method="post">
<label>Phone number:
<input type="phone" id="phone" name="phone" />
</label>
<label>Username (optional):
<input type="text" id="username" name="username" />
</label>
<input type="submit" value="Invite" id="invite_btn" />
<h3 id="feedback"></h3>
</form>
</main>
Der Inhalt von <main> in signup.html sollte sehr ähnlich sein, mit dem Unterschied, dass Sie dort auch eine PIN abfragen werden:
<main>
<h2>
Sign up or log in
</h2>
<form action="/login" method="post">
<label>Phone number:
<input type="phone" id="phone" name="phone" />
</label>
<label>Username:
<input type="text" id="username" name="username" />
</label>
<label>PIN:
<input type="text" id="pin" name="pin" />
</label>
<input type="submit" value="Sign up" id="signup_btn" />
<h3 id="feedback"></h3>
</form>
</main>
Wenn Sie die Standard-HTML-Struktur beibehalten, können Sie weiterhin Links client.js über beide Seiten hinweg. Da die Formulare auf diesen Seiten ein ähnliches Design haben, kann eine einzige Skriptdatei, client.jsdie Funktionalität beider Seiten effizient verwalten. Nach dem Zurücksetzen client.js auf einen leeren Zustand zurückgesetzt, ist dies der perfekte Ort, um Ihr angepasstes Skript zu implementieren.
Beginnen Sie in diesem Skript mit der Identifizierung und Erfassung der für die Interaktion wichtigen Formularelemente. Als Nächstes erkennen Sie die Übermittlungsschaltflächen auf beiden Formularen und richten Listener für Klickereignisse ein. Diese Listener rufen eine gemeinsame Funktion auf, die die Übermittlung der Formulardaten an den Server abwickelt und gleichzeitig das Standardverhalten der Formularübermittlung abfängt und verhindert. Obwohl es möglich ist, die Übermittlung von Formularen ausschließlich mit HTML zu verwalten, bietet die Verwendung von JavaScript für diese Aufgabe mehr Flexibilität, insbesondere wenn sich Ihre Anwendung weiterentwickelt und eine komplexere Handhabung erfordert.
let phone = document.querySelector('#phone');
let username = document.querySelector('#username');
let feedback = document.querySelector('#feedback');
// Invite Form
let invite_btn = document.querySelector('#invite_btn');
if (invite_btn) {
invite_btn.onclick = function(e) {
e.preventDefault();
let body = JSON.stringify({
phone: phone.value,
username: username.value
});
goFetch('/invite', body, e.target);
return false;
};
}
// Sign Up From
let signup_btn = document.querySelector('#signup_btn');
if (signup_btn) {
signup_btn.onclick = function(e) {
e.preventDefault();
let body = JSON.stringify({
phone: phone.value,
username: username.value,
pin: document.querySelector('#pin').value
});
goFetch('/login', body, e.target);
return false;
};
}
function goFetch(url, body, btn) {
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: body
})
.then(response => response.json())
.then(data => {
feedback.innerText = data.message || 'Thank you!';
btn.style.display = 'none';
});
}
Das Projekt ausführen
Um das Projekt zu starten, führen Sie 'npm run start' von Ihrem Terminal aus auf demselben Port aus, auf dem Sie ngrok laufen lassen.
Hinweis: Ihre Anwendung ist fertig, aber es gibt einen Haken: Sie benötigen Administratorrechte, um andere einzuladen, doch um diese Rechte zu erhalten, ist eine Einladung erforderlich. Die Lösung dieses Problems könnte darin bestehen, eine Umgehung für Entwickler (wie Sie) mit vollständigem Zugriff auf den Code zu schaffen. Eine schnelle Lösung besteht darin, den Code der
isAdminFunktion zureturn truezu ändern, die vorübergehend uneingeschränkten Zugriff erlaubt.
Schlussfolgerung und nächste Schritte
Sie haben das Ende dieses Tutorials erreicht! Das Hinzufügen einer Fehlerbehandlung wird die Benutzerinteraktionen verfeinern, z. B. die Auswahl eines bereits verwendeten Benutzernamens oder die Eingabe ungültiger Zeichen. Darüber hinaus erleichtert die Einrichtung von Endpunkten für die Benutzerverwaltung zusammen mit Strategien für die Verwaltung von Berechtigungslisten und die Verlängerung der Sitzungsdauer auf der Grundlage der Benutzeraktivität oder durch eine Verlängerungsfunktion den kontinuierlichen Zugang, während die Abhängigkeit von regelmäßigen SMS-Überprüfungen minimiert wird.
Der Code für dieses Lernprogramm ist auf GitHub verfügbar.
Um die neuesten Nachrichten zu erhalten, verbinden Sie sich mit uns in unserer Developer Gemeinschaft Slack, auf X, früher bekannt als Twitterund bei Veranstaltungen.
