https://d226lax1qjow5r.cloudfront.net/blog/blogposts/trusted-group-authentication-with-sms-and-express/group-authentication.png

Authentification de groupe de confiance par SMS et Express

Temps de lecture : 12 minutes

Introduction

Travailler seul sur un projet peut parfois s'avérer fastidieux et solitaire. C'est pourquoi collaborer avec des amis peut rendre le processus beaucoup plus agréable ! Cependant, organiser des réunions et trouver des horaires qui conviennent à tout le monde peut s'avérer fastidieux. Heureusement, il existe une solution : créer une application web qui vous permet de travailler ensemble en ligne de manière transparente.

Ce tutoriel vous guidera dans la création d'un flux d'authentification simple à l'aide de l'API Verify de Vonage. Cela permettra à vos amis d'accéder en toute sécurité à l'application collaborative sans avoir à gérer des politiques de mots de passe complexes ou de cryptage. Au lieu de cela, vous utiliserez leurs numéros de téléphone et le système de vérification de Vonage pour les authentifier et les garder connectés via des cookies de session.

En bref si vous souhaitez aller de l'avant et déployer immédiatement l'application, vous pouvez trouver tout le le code nécessaire sur GitHub.

Conditions préalables

Avant d'entrer dans le vif du sujet, assurez-vous d'avoir.. :

  1. Node.js et npm installés

  2. Une connaissance de base d'Express.js et de SQLite

  3. A Compte API Vonage

  4. Un numéro virtuel de Vonage avec des capacités SMS

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

This tutorial also uses a virtual phone number. To purchase one, go to Numbers > Buy Numbers and search for one that meets your needs.

En option, vous pouvez utiliser un outil comme ngrok pour exposer publiquement votre serveur de développement local pendant les tests.

Mise en place

Commencez par créer un nouveau projet Node.js et installez les paquets nécessaires :

npm init npm install express sqlite3 connect-sqlite3 cookie-parser express-session @vonage/server-sdk

Cela ajoutera Express, la prise en charge de la base de données SQLite3, l'intergiciel pour les sessions/cookies et la bibliothèque client Vonage API.

Ensuite, créez un fichier .env et ajoutez vos identifiants Vonage et votre numéro virtuel :

API_KEY="YOUR_API_KEY" API_SECRET="YOUR_API_SECRET" APP_NUM="YOUR_VIRTUAL_NUMBER"

Vous devrez également générer une clé secrète pour le stockage de votre session :

SESH_SECRET="a_very_complex_random_string"

Définissez toute autre variable d'environnement dont vous pourriez avoir besoin, comme un code d'invitation, un domaine de projet, l'ID de l'application Vonage, etc.

ADMINS="kelly,michelle,beyonce" INVITE_CODE="code123" PRIVATE_KEY_PATH=private.key APP_ID=49f7d24b-42343ds-vs234-vxcsfds PORT=3003

Configurer Express

Dans le fichier principal de votre serveur (par exemple..., app.js), exigez Express et mettez en place le serveur de base :

const express = require('express');
const app = express();
const port = 3000;
 
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

Ensuite, ajoutez un intergiciel pour analyser les demandes JSON, gérer les sessions et activer le stockage des sessions SQLite3 :

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

N'oubliez pas d'initialiser l'objet SDK Vonage Server avec vos identifiants API :

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

Configuration de la base de données SQLite

Vous aurez besoin de quelques tables SQLite pour stocker les données et les sessions des utilisateurs pendant le processus d'authentification :

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

La table Sessions contient temporairement les numéros de téléphone des utilisateurs et les identifiants de demande Vonage pendant la vérification. Le tableau Allowlist suit les inscriptions autorisées, éventuellement avec un nom d'utilisateur pré-approuvé. Authors est la liste persistante des noms d'utilisateur authentifiés.

Routes des poignées

Des itinéraires pour vos points de vue

Votre serveur dispose déjà d'une route pour la page principale à l'adresse /. En dessous, vous pouvez ajouter deux autres routes pour une page d'inscription ou de connexion pour vos utilisateurs et une page d'administration pour vous :

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

Pour commencer à gérer l'accès à votre fonctionnalité d'administration, vous devez également déclarer la fonction isAdmin référencée dans votre /admin route. Elle divisera votre liste d'administrateurs de .env dans un tableau et recherchera une correspondance exacte avec le nom d'utilisateur dans la session en cours :

function isAdmin(sesh) {
  let admins = process.env.ADMINS.split(',');
  return admins.includes(sesh.username);
}

Le point d'accès Admin

La première étape du processus d'ajout d'un utilisateur consiste, pour un administrateur, à ajouter son numéro de téléphone à une liste d'autorisation. Étant donné que la même personne peut vouloir se connecter à partir de différents appareils (ce qui nécessite des cookies de session supplémentaires), ou que sa session peut expirer, l'administrateur peut éventuellement associer le numéro de téléphone à un nom d'utilisateur existant.

Pour commencer, déclarez le point de terminaison à /invite et ajouter un contrôle de sécurité pour s'assurer que cette personne est toujours un administrateur :

app.post('/invite', function(request, response) {
  if (!isAdmin(request.session)) {
    response.status(500).send({message: "Sorry, you're not an admin"});
    return;
  }
 
});

Si la personne qui tente d'ajouter une invitation n'est pas un administrateur, la demande doit échouer.

La fonction récupère ensuite le numéro de téléphone à partir de la demande. Après un léger contrôle de validité, il est ajouté à la liste d'autorisation. Si l'administrateur a spécifié un nom d'utilisateur, celui-ci sera également ajouté :

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

Une fois le nouveau numéro de téléphone ajouté à la liste d'autorisation, la dernière chose à faire est d'envoyer une invitation par SMS au nouvel utilisateur. Le texte sera envoyé par le numéro de téléphone que vous avez enregistré dans .envet l'utilisateur recevra le code d'invitation actuel à envoyer en réponse.

Vous pouvez sauter cette étape et envoyer le code PIN de vérification. Cependant, cela vous permet de fournir toute information contextuelle dont l'utilisateur pourrait bénéficier, comme l'URL d'inscription. Comme les NIP de vérification de Vonage ne sont valables que pendant cinq minutes, cela permet également de s'assurer que le NIP du destinataire n'expire pas avant qu'il ne l'ait vu :

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

Le point de terminaison Webhook

Avant de pouvoir recevoir des textes entrants avec l'API Messages de Vonage, nous devons configurer les paramètres de notre application dans le tableau de bord de Vonage. Nous allons acheter un numéro virtuel et définir l'URL Inbound Webhook sous la configuration du numéro à notre URL locale ngrok qui expose notre serveur Node.js.

Par exemple, nous pouvons définir l'URL du Webhook entrant comme suit http://1234abc.ngrok.io/answer pour envoyer des messages entrants à notre point de terminaison local /answer.

Une fois votre numéro de téléphone configuré, vous pouvez ajouter la logique pour le point de terminaison du webhook. Vous vérifierez que le texte contient le code d'invitation actuel et que le numéro de téléphone d'où il provient figure dans la liste des numéros autorisés. Si ces conditions sont remplies, vous enverrez une demande de vérification et enregistrerez le numéro de téléphone et l'identifiant obtenus en réponse dans votre base de données Sessions :

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

Cette fois, le nouvel utilisateur recevra un texte généré automatiquement par Vonage Verify contenant son code PIN. Vous avez fourni le numéro de téléphone de votre application et le domaine de ngrok à identifier, mais à part cela, le texte est passe-partout. L'utilisateur doit pouvoir répondre à quelque chose dans ce message. Une fois l'enregistrement stocké, nous attendrons que l'utilisateur termine l'étape finale via l'application web.

Le point final d'inscription ou de connexion

Le nouvel utilisateur enverra son numéro de téléphone, son nom d'utilisateur et son code PIN à partir du client web. Seul le nom d'utilisateur sera stocké. Les autres valeurs sont destinées au processus d'authentification et, si la connexion réussit, elles seront supprimées du magasin de données.

Ajoutez un nouveau /login à votre serveur, et comme première étape, faire une validation rapide du nom d'utilisateur. L'exemple ici n'autorise que les caractères de base, ce qui peut convenir à vos besoins, ou vous pouvez vouloir un ensemble d'options plus robustes. Une fois le nom d'utilisateur validé, vous trouverez la session pour le numéro de téléphone fourni :

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) {
 
  }); 
});

Dans le callback fournissant la ligne de session, vous vérifierez à nouveau le nom d'utilisateur : cette fois, vous verrez s'il est déjà utilisé et, si c'est le cas, si ce numéro de téléphone est autorisé à se connecter avec lui :

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) {
 
    }
  }); 
});

Si les vérifications du nom d'utilisateur sont toutes réussies, vous utiliserez un indicateur pour confirmer que vous pouvez continuer et que le code PIN reçu du client est correct pour cette demande de vérification. Si c'est le cas, vous obtiendrez un statut de 0 dans la réponse et vous pourrez alors supprimer en toute sécurité la session et les enregistrements de la liste d'autorisations que vous avez utilisés au cours de ce processus. La dernière étape consiste à mettre le nom d'utilisateur dans la session :

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

Ajouter des balises

Pour collecter des données du côté client, vous aurez besoin de deux formulaires similaires : un formulaire d'administration et un formulaire d'inscription. Le formulaire d'administration déclenchera des invitations pour les nouveaux utilisateurs et le formulaire d'inscription créera de nouvelles sessions. Votre page index.html sera la page d'accueil de votre projet ; vous pouvez l'utiliser pour fournir toutes les informations ou fonctionnalités que vous souhaitez. Toutefois, il peut être utile de copier son contenu dans les pages admin.html et signup.html afin de mettre en place votre échafaudage.

Dans la balise <main> dans admin.html, remplacez le HTML par un simple formulaire permettant de collecter un numéro de téléphone et un nom d'utilisateur :

<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>

Le contenu de <main> dans le fichier signup.html devrait être très similaire, à ceci près que vous recueillerez également un code PIN :

<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>

En conservant la structure HTML par défaut, vous continuerez à créer des liens entre les deux pages. client.js entre les deux pages. Étant donné que les formulaires de ces pages ont une conception similaire, un seul fichier script, client.jspeut gérer efficacement leurs fonctionnalités. Après avoir réinitialisé client.js à l'état vierge, c'est l'endroit idéal pour implémenter votre script personnalisé.

Dans ce script, commencez par identifier et collecter les éléments de formulaire essentiels à l'interaction. Ensuite, détectez les boutons de soumission sur les deux formulaires et établissez des récepteurs d'événements de clic. Ces récepteurs invoqueront une fonction commune qui gère la soumission des données du formulaire au serveur tout en interceptant et en empêchant le comportement standard de soumission du formulaire. Bien qu'il soit possible de gérer les soumissions de formulaires uniquement avec HTML, l'utilisation de JavaScript pour cette tâche offre une plus grande flexibilité, en particulier lorsque votre application évolue et exige un traitement plus sophistiqué.

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

Exécuter le projet

Pour démarrer le projet, lancez 'npm run start' depuis votre terminal sur le même port que celui où vous avez lancé ngrok.

Note : Votre application est prête, mais il y a un hic : vous avez besoin de droits d'administrateur pour inviter d'autres personnes, or l'obtention de ces droits nécessite une invitation. Pour résoudre ce problème, il pourrait être nécessaire de créer une solution de contournement pour les développeurs (comme vous) qui ont un accès complet au code. Une solution rapide consiste à modifier la fonction isAdmin de votre code en return truepermettant temporairement un accès illimité.

Conclusion et prochaines étapes

Vous avez atteint la fin de ce tutoriel ! L'ajout d'une gestion des erreurs permettra d'affiner les interactions avec les utilisateurs, comme la sélection d'un nom d'utilisateur déjà utilisé ou la saisie de caractères non valides. En outre, la mise en place de points finaux de gestion des utilisateurs, ainsi que de stratégies de gestion des listes d'autorisation et de prolongation de la durée de vie des sessions en fonction de l'activité de l'utilisateur ou par le biais d'une fonction de renouvellement, facilitera l'accès continu tout en minimisant la dépendance à l'égard des vérifications régulières par SMS.

Le code de ce tutoriel est disponible sur GitHub.

Pour obtenir les dernières nouvelles, connectez-vous avec nous sur notre communauté de développeurs Communauté des développeurs Slacksur X, anciennement connu sous le nom de Twitteret lors d'événements.

Partager:

https://a.storyblok.com/f/270183/400x400/3f6b0c045f/amanda-cavallaro.png
Amanda CavallaroDéfenseur des développeurs