https://d226lax1qjow5r.cloudfront.net/blog/blogposts/add-2fa-to-nuxt-with-nexmo-verify-dr/poster-image-1.png

Ajoutez 2FA à vos Applications Nuxt avec Nexmo Verify

Publié le April 19, 2021

Temps de lecture : 18 minutes

Dans ce tutoriel, nous allons construire une application de base avec une authentification à deux facteurs en utilisant la technologie Nuxt JS Nuxt JS.

Notre application contient une section secrète qui n'est accessible que si l'utilisateur se vérifie en entrant un code PIN qui lui est envoyé par SMS.

Si vous souhaitez consulter tout le code de cet exemple, vous pouvez jeter un coup d'œil au fichier nexmo-verify-nuxt sur notre Nexmo Community GitHub.

Conditions préalables

Si tu veux suivre le cours, tu auras besoin des éléments suivants :

  • Node JS - cette application a été réalisée avec la version 10.0, mais elle devrait fonctionner avec la version 8 ou supérieure.

  • Une expérience préalable avec VueJS serait utile mais pas absolument nécessaire. Il s'agit d'une application très basique qui constitue une bonne introduction pour les nouveaux apprenants.

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.

Introduction à Nuxt

Nuxt est un framework permettant de construire des " Applications Vue.js universelles ". Essentiellement, cela signifie que lorsque vous utilisez Nuxt, vous avez la possibilité de construire votre application finie pour la production de trois manières différentes :

  1. Il s'agit d'une application de rendu côté serveur qui construit tout le code HTML à rendre dans le navigateur côté serveur au moment de l'exécution et l'envoie ensuite à l'utilisateur.

  2. Comme une application à page unique où tout le rendu de l'interface utilisateur a lieu dans le navigateur au moment de l'exécution. C'est ce qui ressemble le plus à une application Vue JS traditionnelle.

  3. En tant que site statique, où tout le HTML, le CSS et le JS sont construits dans des fichiers statiques que vous pouvez télécharger sur Amazon S3, GitHub Pages, Netlify - partout où vous le souhaitez !

C'est un framework très flexible avec lequel, en tant que fan de Vue JS, j'ai vraiment apprécié de travailler. Si vous voulez en savoir plus, consultez l'introduction à Nuxt JS dans leur documentation.

Pourquoi utiliser Nuxt dans ce cas ?

J'ai choisi Nuxt pour ce tutoriel parce que je voulais mettre en avant quelques fonctionnalités qui m'ont vraiment impressionné et qui ont rendu les possibilités de travailler avec un tel framework presque illimitées. Ces fonctionnalités sont les suivantes :

  • Rendu côté serveur

  • Logiciel médiateur de serveur personnalisé

Ne vous inquiétez pas trop si vous ne savez pas encore de quoi il s'agit. Nous les aborderons bien assez tôt. Commençons par le code de notre application en installant Nuxt et en construisant notre structure de dossiers.

Structurer l'application

Commencez par créer un dossier vide dans lequel vous travaillerez, appelez-le comme vous le souhaitez.

Nous allons utiliser NPM pour installer les dépendances de ce projet, alors commencez par exécuter :

npm init -y # The -y flag will skip through the questions

Installez ensuite les dépendances :

npm install nuxt express jsonwebtoken axios nexmo@beta

Nous travaillerons également avec l'excellent dotenv dans le développement pour gérer nos variables d'environnement, donc installez-le en tant que dépendance dev.

npm install -D dotenv

Avant d'entrer dans la structure des dossiers de l'application, il y a un petit changement à faire. Ouvrez package.json dans votre éditeur et remplacez la section scripts par ceci :

"scripts": {
  "dev": "nuxt",
  "build": "nuxt build",
  "start": "nuxt start"
}

Ajout de la structure du dossier

Nuxt utilise un ensemble de dossiers spécifiques pour s'organiser. Souvent, la présence de ces dossiers garantit que les dépendances nécessaires à la réalisation de certaines choses sont automatiquement incluses lorsque vous créez votre application.

Par exemple, si vous devez utiliser VueX dans votre application pour gérer l'état partagé, vous n'avez pas besoin de l'installer. Créez simplement un dossier nommé storeet d'y placer un index.js à l'intérieur et Nuxt inclura automatiquement VueX pour vous. C'est plutôt sympa.

Nous Nous allons Nous allons utiliser VueX, et quelques autres choses dans notre application, donc, à la racine de votre répertoire de travail, créez quelques nouveaux dossiers :

mkdir pages store layouts middleware node-scripts assets

Les pages, store, et middleware sont spécifiques à Nuxtc'est à cela qu'ils servent :

  • pages - C'est là que vous gardez les pages de votre site. Nuxt supporte génération automatique d'itinéraires qui s'aligne sur la structure de vos dossiers. Par exemple, pour atteindre https://myapp.xyz/register, vous devez avoir un dossier appelé register à l'intérieur du dossier pages à l'intérieur du dossier

  • store - Ce dossier contient les fichiers de votre boutique VueX. VueX sera automatiquement inclus si vous avez ce dossier.

  • middleware - Si vous souhaitez définir des fonctions personnalisées à exécuter avant le rendu des pages, c'est ici qu'elles doivent se trouver.

Si vous souhaitez en savoir plus sur la façon dont la structure des dossiers correspond aux actions dans Nuxt, consultez le site suivant Structure du répertoire de Nuxt.

Config, Styling, Store & The Default Layout

Parce qu'il est préférable de se concentrer sur l'aspect central de ce tutoriel, je vais vous suggérer de copier certains aspects directement à partir du référentiel ou de blocs de code pour des raisons de rapidité.

À la racine de votre répertoire de travail, créez un nouveau fichier appelé ed nuxt.config.js et ouvrez-le. Ajoutez le code suivant :

module.exports = {
  head: {
    meta: [
      { name: 'viewport', content: 'width=device-width, initial-scale=1.0' }
    ],
    script: [
      {
        src:
          'https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-rc.2/js/materialize.min.js',
        body: true
      }
    ],
    link: [
      {
        rel: 'stylesheet',
        href: 'https://fonts.googleapis.com/icon?family=Material+Icons'
      },
      {
        rel: 'stylesheet',
        href:
          'https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-rc.2/css/materialize.min.css'
      }
    ]
  },
  loading: false,
  build: {
    vendor: ['axios']
  },
  env: {
    baseUrl: process.env.BASE_URL || 'http://localhost:3000'
  }
};

Cela permettra de s'assurer que tous les styles et polices que j'ai utilisés fonctionneront également sur votre version. Nous utilisons Materialize CSS comme cadre d'interface utilisateur. leur documentation si vous avez besoin de plus d'informations sur l'un des éléments de la mise en page.

Pour plus d'informations sur la configuration de Nuxt, consultez leur guide.

Ensuite, ouvrez le dossier layouts et créez un nouveau fichier appelé default.vue. Ajoutez-y la balise suivante :

<template>
  <div class="container">
    <nuxt/>
  </div>
</template>

Cela permettra de s'assurer que toutes les autres vues que nous créons sont correctement contenues et disposées.

Cette application utilise une certaine gestion de l'état, donc dans le dossier store créez un fichier appelé index.js et ajoutez le code suivant :

import Vuex from 'vuex';

export const store = () => {
  return new Vuex.Store({
    state: {
      token: null
    },
    mutations: {
      SET_TOKEN(state, token) {
        state.token = token || null;
      },
      INVALIDATE_TOKEN(state) {
        state.token = null;
      }
    },
    getters: {
      isVerified(state) {
        return state.token;
      }
    }
  });
};

export default store;

Une fois que tout cela est en place, il est temps d'écrire du code !

Créer les pages

L'application que nous construisons comporte deux pages. La première est une page d'atterrissage avec un formulaire de connexion, la seconde est une section secrète à laquelle les utilisateurs peuvent accéder une fois qu'ils ont été vérifiés.

Nous allons d'abord travailler sur la page d'atterrissage. Dans le répertoire pages créez un nouveau fichier appelé index.vue.

La page d'atterrissage

Ouvrez le fichier index.vue dans votre éditeur et ajoutez le code suivant :

<template>
  <div>
    <div class="row">
      <div class="col s12 center-align">
        <h1><i class="medium material-icons">verified_user</i> Login</h1>
      </div>
    </div>
    <div class="row" v-if="request.token === ''">
      <form v-on:submit.prevent class="col s12">
      <div class="row center-align">
        <div class="input-field col s12">
          <i class="material-icons prefix">phone</i>
          <input type="text" id="phoneNumber" v-model="phoneNumber"/>
          <label for="phoneNumber"> Phone Number</label>
        </div>
        <div class="row center-align">
          <button v-on:click.stop.prevent="sendVerificationCode" type="submit" class="waves-effect waves-light btn"><i class="material-icons left">account_box</i>Send me a verifiation code</button>
        </div>
      </div>
    </form>
    </div>
    <div class="row" v-else>
      <form v-on:submit.prevent class="col s12">
        <div class="row center-align">
          <div class="input-field col s12">
            <i class="material-icons prefix">sms</i>
            <input type="text" id="verificationPin" v-model="request.verificationPin"/>
            <label for="verificationPin"> Enter the pin you were sent</label>
          </div>
          <div class="row center-align">
            <button v-on:click.stop.prevent="verifyPin" type="submit" class="waves-effect waves-light btn"><i class="material-icons left">account_box</i>Verify me</button>
          </div>
        </div>
      </form>
    </div>
  </div>
</template>

<script>
import { mapMutations } from 'vuex';
import axios from 'axios';

export default {
  data() {
    return {
      phoneNumber: '',
      request: {
        token: '',
        verificationPin: ''
      }
    };
  },
  methods: {
    displayMessage: function(type, message) {
      if (type === 'error') {
        M.toast({
          html: `${message}`,
          classes: 'rounded red accent-1',
          displayTime: 3000
        });
      }
    },

    sendVerificationCode: async function() {
      const { data } = await axios.post('/verification/send', {
        phoneNumber: this.phoneNumber
      });
      if (!data.token) {
        this.displayMessage('error', data.error_text || data.error);
      } else {
        const { token } = data;
        this.request.token = token;
      }
    },

    verifyPin: async function() {
      const { data } = await axios.post('/verification/verify', {
        token: this.request.token,
        code: this.request.verificationPin
      });
      if (!data.token) {
        this.displayMessage('error', data.error_text || data.error);
      } else {
        const { token } = data;
        this.$store.commit('SET_TOKEN', token);
        this.$nuxt.$router.replace({ path: '/secret' });
      }
    }
  }
};
</script>

Ce fichier comporte deux sections, <template> et <script>. La section <template> contient le code HTML de notre formulaire de connexion et la section <script> contient toutes les méthodes dont nous avons besoin pour faire des choses avec ce formulaire.

Nous exposons ici trois méthodes, dans l'ordre, voici ce qu'elles font :

  • displayMessage - Il s'agit simplement d'une fonction d'aide qui affichera toutes les erreurs renvoyées par l'API dans un message d'erreur superposé. Les classes que vous voyez ici proviennent directement de Materialize CSS.

  • sendVerificationCode - Prend le numéro de téléphone saisi par l'utilisateur et le transmet à notre API middleware pour travailler avec Nexmo Verify.

  • verifyPin - Lorsque l'utilisateur reçoit son code PIN, il doit également le saisir ici, cette méthode le transmet au point de terminaison Verify de notre API middleware.

Vous pouvez alors lancer l'application :

npm run dev

Si tout va bien, vous devriez pouvoir vous rendre sur le site https://localhost:3000 et voir ceci :

Log in Page

Malheureusement, cliquer sur le bouton "Envoyez-moi un code de vérification" ne servira à rien car notre code essaie de transmettre le numéro à un point de terminaison appelé /verification/send qui n'existe pas.

Il est temps de passer au côté serveur.

Ajouter une mini API de vérification

L'API Nexmo Verify API Nexmo Verify exige que vous utilisiez une clé et un secret pour authentifier les requêtes. Dans les applications typiques rendues par le navigateur, ou les applications à page unique, nous ne pourrions pas faire cela sans exposer notre clé secrète au monde entier - un désastre.

La solution habituelle consiste à créer un script NodeJS rapide qui expose quelques points d'accès Express que vous pouvez utiliser pour réaliser ce que vous voulez tout en gardant vos clés secrètes.

L'inconvénient est qu'à moins que votre application n'ait vraiment besoin d'une API volumineuse avec beaucoup de points d'extrémité et de fonctionnalités côté serveur, trouver un endroit pour héberger ce script en plus de votre application Nuxt représente, à mon avis, plus d'effort que nécessaire.

Saviez-vous que vous pouviez exécuter Express dans Nuxt ?

C'est vrai. Parce que Nuxt rend déjà les choses côté serveur, techniquement NodeJS est déjà en jeu, ce qui signifie que vous pouvez utiliser des paquets comme Express en tant qu'intergiciel côté serveur.

Cela signifie qu'il n'y a pas de serveur supplémentaire pour notre petite API de vérification !

Un fichier, deux points d'extrémité, le tout côté serveur

Créer un nouveau fichier dans le dossier node-scripts appelé verification_api.js et ouvrez-le dans votre éditeur.

if (process.env.NODE_ENV !== 'production') {
  require('dotenv').config();
}
const express = require('express');
const Nexmo = require('nexmo');
const jwt = require('jsonwebtoken');

const API_KEY = process.env.apiKey;
const API_SECRET = process.env.apiSecret;
const JWT_SECRET = process.env.jwtSecret;

const app = express();
app.use(express.json());

const nexmo = new Nexmo({ apiKey: API_KEY, apiSecret: API_SECRET });

app.post('/send', async (req, res) => {
  // Get the phone number from the request body of our main app
  let phoneNumber = req.body.phoneNumber;

  nexmo.verify.request(
    {
      number: phoneNumber,
      brand: 'MyApp',
      code_length: 6,
      pin_expiry: 60
    },
    async (err, result) => {
      if (err || result.status !== '0') {
        res.json({ error: err || result.error_text });
      } else {
        const accessToken = await jwt.sign(
          {
            phoneNumber,
            request_id: result.request_id
          },
          JWT_SECRET
        );
        res.json({ token: accessToken });
      }
    }
  );
});

app.post('/verify', async (req, res) => {
  let { token, code } = req.body;
  const tokenObject = await jwt.verify(token, JWT_SECRET);

  nexmo.verify.check(
    { request_id: tokenObject.request_id, code: code },
    async (err, result) => {
      if (err || result.status !== '0') {
        res.json({ error: err || result.error_text });
      } else {
        const tokenizeResult = await jwt.sign(result, JWT_SECRET);
        res.json({ token: tokenizeResult });
      }
    }
  );
});

app.post('/auth-check', async (req, res) => {
  let { token } = req.body;
  const tokenObject = await jwt.verify(token, JWT_SECRET);
  const { request_id } = tokenObject;

  nexmo.verify.search(request_id, async (err, result) => {
    if (err) {
      res.json({ error: err });
    } else {
      res.json(result);
    }
  });
});

module.exports = {
  path: '/verification',
  handler: app
};

Ceux d'entre vous qui sont familiers avec les applications Node et Express se sentiront à l'aise avec ce code.

Il y a trois points d'arrivée, /send, /verify et /auth-check sont exposés. Voici ce qu'ils font :

  • /send - Reçoit le numéro de téléphone de l'utilisateur à partir du formulaire et crée une nouvelle demande de vérification en utilisant le Nexmo Node SDK. Le request_id de cette nouvelle demande est converti en un jeton Web JSON signé et renvoyé au front-end.

  • /verify - Reçoit le JWT et le code pin de l'utilisateur. Le JWT est décodé et l'original est extrait, puis transmis avec le code pin à l'API Verify pour voir s'ils correspondent. request_id est extrait, puis transmis avec le code pin à l'API Verify pour voir s'ils correspondent. Si c'est le cas, la vérification est réussie !

  • /check-auth - Est une méthode d'aide utilisée pour vérifier si une autorisation existe déjà pour le numéro de téléphone de l'utilisateur. request_idafin que les utilisateurs n'aient pas à saisir leur numéro à chaque fois.

Contrairement à une application express typique où l'on mettrait server.listen à un port, nous allons exporter tout notre script en tant que module afin que Nuxt puisse y faire référence.

Avant que cela ne fonctionne, créez un fichier appelé .env à la racine de votre répertoire de travail et ajoutez-y ce qui suit :

apiKey = "YOUR NEXMO API KEY GOES HERE"
apiSecret = "YOUR NEXMO API SECRET GOES HERE"
jwtSecret = "ANY RANDOM STRING OF LETTERS & NUMBERS GOES HERE"

Note : Veillez à ce que .env ne fasse pas partie des dépôts GitHub en ajoutant également un fichier .gitignore à votre répertoire de travail. Vous pouvez copier celui que j'ai créé pour ce projet.

Montage d'un intergiciel côté serveur

Pour rendre nos nouveaux points de terminaison accessibles dans notre application Nuxt, il y a deux étapes à franchir.

La première consiste à enregistrer le script en tant qu'intergiciel dans le fichier nuxt.config.js. Pour ce faire, ajoutez la ligne suivante :

serverMiddleware: ['~/node-scripts/verification_api'];

Si vous avez besoin de vérifier exactement où cela doit aller, vous pouvez référencer l'exemple sur GitHub.

La deuxième étape consiste à construire l'application pour que ces nouveaux itinéraires (/verification/send et /verification/verify) soient disponibles. Pour ce faire, exécutez la commande suivante dans votre terminal :

npm run build

Une fois cette opération terminée, redémarrez le serveur de développement :

npm run dev

Allez sur https://localhost:3000 et entrez votre numéro dans le formulaire. N'oubliez pas d'indiquer également l'indicatif du pays.

En cliquant sur "Send me a Verification Code", vous recevrez un SMS contenant un code pin à 6 chiffres. Vous remarquerez également que l'affichage a changé et qu'il attend maintenant le code PIN.

Verification page

La saisie du code PIN finalisera la vérification et si tout se passe bien, l'application tentera de rediriger vers un itinéraire appelé /secret.

...qui n'existe pas encore. Créons-le et assurons-nous qu'il est sécurisé à l'aide d'un intergiciel Nuxt.

Sécuriser notre page secrète

Créez un nouveau dossier à l'intérieur du dossier pages et l'appeler secret et ajoutez-y un fichier appelé index.vue à l'intérieur du dossier.

Ouvrez index.vue dans votre éditeur et ajoutez le code suivant :

<template>
  <div>
    <div class="row center-align">
      <h1>Secret Area</h1>
    </div>
    <div class="row center-align">
      <button v-on:click.stop.prevent="logout" class="waves-effect waves-light btn red"><i class="material-icons left">account_box</i>Logout</button>
    </div>
  </div>
</template>

<script>
export default {
  middleware: 'check_auth',
  methods: {
    // Clicking log out triggers this function that wipes out any pre-existing tokens
    logout: function() {
      this.$store.commit('INVALIDATE_TOKEN');
      this.$nuxt.$router.replace({ path: '/' });
    }
  }
};
</script>

Vous remarquerez que ce code fait référence à un logiciel intermédiaire appelé check_auth. Cet intergiciel sera appelé chaque fois que ce fichier sera demandé, et peut donc être utilisé pour sécuriser la page.

Créer l'intergiciel d'authentification

Dans le dossier middleware créez un nouveau fichier appelé check_auth.js et ouvrez-le dans votre éditeur. Ajoutez le code suivant :

import axios from 'axios';

export default function({ store, route, redirect }) {
  if (store.getters.isVerified) {
    const token = store.getters.isVerified;
    axios
      .post('/verification/auth-check', {
        token
      })
      .then(res => {
        const { data } = res;

        if (!data.error_text && data.checks[0].status === 'VALID') {
          console.log('valid, allowing access');
          redirect('/secret');
        } else {
          console.log('invalid, redirecting...');
          redirect('/');
        }
      })
      .catch(err => console.log(err));
  } else {
    redirect('/');
  }
}

Ce fichier sera appelé chaque fois que /secret est demandé en tant qu'itinéraire, mais avant que le code HTML ne soit rendu et envoyé au navigateur. Au-dessus, les étapes suivantes ont lieu :

  • Le magasin VueX est vérifié à l'aide d'une méthode getter pour voir si un jeton Web JSON a déjà été renvoyé par notre API de vérification.

  • Si c'est le cas, nous le transmettons au point de terminaison /auth-check pour voir si l'authentification est toujours active.

  • S'il n'existe pas ou si l'authentification n'est pas valide, nous redirigeons la demande vers le formulaire de connexion.

Si votre serveur de développement est toujours en cours d'exécution à ce moment-là, redémarrez-le pour que l'intergiciel s'enregistre correctement et répétez le processus de connexion. Cette fois, une vérification réussie devrait vous montrer la page /secret page.

(La page secrète que j'ai créée pour l'application d'exemple est plus amusante que celle-ci. Vous pouvez l'obtenir ici si vous souhaitez l'utiliser).

Conclusion

Nuxt nous permet de prendre une technologie que nous connaissons déjà, comme Vue, et d'y ajouter des éléments puissants, tels que l'authentification via un intergiciel, sans avoir à travailler sur des API distinctes sur des serveurs différents.

De toute évidence, il y a des limites à ce que vous voulez intégrer dans le middleware côté serveur avant de passer à la construction d'une API externe, ce qui serait la meilleure option. C'est à vous de décider, mais je dirais que plus de 5 points d'extrémité de base peuvent justifier l'effort. Si c'est moins, envisagez de le construire en tant qu'intergiciel - surtout s'il s'agit juste d'un proxy pour les appels à une autre API comme le fait cet exemple.

J'espère que cela vous a donné un bon aperçu de ce qui peut être fait, et sachez que cette approche fonctionne pour beaucoup d'API Nexmo, et pas seulement pour Verify. Vous pourriez tout aussi bien faire en sorte que votre middleware côté serveur envoie des SMS au lieu de vérifier les utilisateurs.

Soyez créatifs et si vous trouvez d'autres exemples, n'hésitez pas à les partager avec nous via notre canal Slack de la communauté Nexmo.

Partager:

https://a.storyblok.com/f/270183/250x250/d0444194cd/martyn.png
Martyn DaviesAnciens de Vonage

Ancien directeur de la formation des développeurs chez Vonage. Avec une expérience de développeur créatif, de gestionnaire de produits et d'organisateur de journées de hacking, Martyn travaille comme défenseur de la technologie depuis 2012, après avoir travaillé dans le secteur de la radiodiffusion et dans de grandes maisons de disques. Il forme et responsabilise les développeurs du monde entier.