Passwordless Authentication

Passwords can be hard to remember and insecure. By implementing passwordless login with the Verify API, you can replace passwords with single-use codes delivered to your user's mobile phone by SMS or a voice call.

This tutorial is based on the Passwordless Authentication use case. You can download the source code from GitHub.

In this tutorial

We will build a simple application that uses Node.js and the Nexmo Verify API to authenticate a user without requiring them to use a password.

The following sections explain the code in this tutorial. They show you how to:

Prerequisites

To work through this tutorial you need:

Create the basic web application

The application uses the Express framework for routing and the pug templating system for building the UI.

Initialize dependencies

In addition to express and pug, we will use the following external modules:

We initialize the dependencies and start the web server in server.js:

require('dotenv').load();

const path = require('path')
const express = require('express');
const session = require('express-session');
const bodyParser = require('body-parser');
const app = express();
const Nexmo = require('nexmo');

const NEXMO_API_KEY = process.env.NEXMO_API_KEY;
const NEXMO_API_SECRET = process.env.NEXMO_API_SECRET;
const NEXMO_BRAND_NAME = process.env.NEXMO_BRAND_NAME;

let verifyRequestId = null;
let verifyRequestNumber = null;

// Location of the application's CSS files
app.use(express.static('public'));

// The session object we will use to manage the user's login state
app.use(session({
    secret: 'loadsofrandomstuff',
    resave: false,
    saveUninitialized: true
}));

app.use(bodyParser.urlencoded({ extended: true }));

// For templating
app.set('view engine', 'pug');

// Run the web server
const server = app.listen(3000, () => {
    console.log(`Server running on port ${server.address().port}`);
});

Define the routes

We will use the following routes in our application:

app.get('/', (req, res) => {
    /*
        If there is a session for the user, the `index.html`
        page will display the number that was used to log
        in. If not, it will prompt them to log in.
    */
});

app.get('/login', (req, res) => {
    // Display the login page
    res.render('login');
});

app.post('/verify', (req, res) => {
    // Start the verification process

    /* 
        Redirect to page where the user can 
        enter the code that they received
     */
    res.render('entercode');
});

app.post('/check-code', (req, res) => {
    // Check the code provided by the user
});

app.get('/logout', (req, res) => {
    req.session.destroy();
    res.redirect('/');
});

Display the login page

In the / route, you want to provide the index.html page with the user's login details (if the user is logged in) and the brand name for the application that you specified in the .env file, for example:

NEXMO_API_KEY=
NEXMO_API_SECRET=
NEXMO_BRAND_NAME=Acme Inc

Code the / route handler as shown below:

app.get('/', (req, res) => {
    /*
        If there is a session for the user, the `index.html`
        page will display the number that was used to log
        in. If not, it will prompt them to log in.
    */
    if (!req.session.user) {
        res.render('index', {
            brand: NEXMO_BRAND_NAME
        });
    }
    else {
        res.render('index', {
            number: req.session.user.number,
            brand: NEXMO_BRAND_NAME
        });
    }
});

This populates the index.html page using the variable interpolation and conditional processing provided by the pug tempating library.

In the HTML you display the user's phone number and:

extends layout.pug

block content
    h1 Welcome to #{brand}!
    if number
        p You are logged in using number: #{number}
        a(href="logout")
            button.ghost-button(type="button") Logout
    else
        p Please log in to continue
        a(href="login")
            button.ghost-button(type="button") Login

Collect the user's phone number

When your user clicks the login button on your homepage, redirect to the /login route and show them the form where they can enter their number:

app.get('/login', (req, res) => {
    // Display the login page
    res.render('login');
});

The login.html page is generated from the login.pug file:

extends layout.pug

block content
    h1 Log in: Step 1
    fieldset
        form(action='/verify', method='post')
            input.ghost-input(name='number', type='text', placeholder='Enter your mobile number', required='')
            input.ghost-button(type='submit', value='Get Verification Code')

Note: The application expects the phone number your users provide to be in E.164 format that includes the country prefix. In a production application you would probably want to format local numbers for them. You can do this using the Number Insight API.

When the user has submitted this form, your application redirects to the /verify route where you will send a verification request using the Nexmo Verify API.

Send the verification request

The verify request starts the verification process by generating a verification code to send to the user. The first one is sent by SMS. If the user fails to respond within a specified time period then the API makes a second and, if necessary, third attempt to deliver the PIN code using a voice call.

To send the verification request, use the nexmo-node REST API client library to your application. Add the following to server.js:

const nexmo = new Nexmo({
    apiKey: NEXMO_API_KEY,
    apiSecret: NEXMO_API_SECRET
}, {
        debug: true
    });

You must configure your API key and secret in the .env file in order to initialize the nexmo library:

NEXMO_API_KEY=YOUR_NEXMO_API_KEY
NEXMO_API_SECRET=YOUR_NEXMO_API_SECRET
NEXMO_BRAND_NAME=Acme Inc

Note: Nexmo recommends that you always store your API credentials in environment variables.

You can then send the verification code to your user in the /verify route by making a verify request to the Verify API:

app.post('/verify', (req, res) => {
    // Start the verification process
    verifyRequestNumber = req.body.number;
    nexmo.verify.request({
        number: verifyRequestNumber,
        brand: NEXMO_BRAND_NAME
    }, (err, result) => {
        if (err) {
            console.error(err);
        } else {
            verifyRequestId = result.request_id;
            console.log(`request_id: ${verifyRequestId}`);
        }
    });
    /* 
        Redirect to page where the user can 
        enter the code that they received
     */
    res.render('entercode');
});

We store the user's phone number and the request_id that the call to the Verify API returns. We will need the request_id to check the code that the user enters into our application. We will display the phone number on the home page if the user logs in successfully.

Collect the verification code

When the user receives the verification code they enter it using the form provided in entercode.html:

extends layout.pug

block content
    h1 Log in: Step 2
    fieldset
        form(action='/check-code', method='post')
            input.ghost-input(name='code', type='text', placeholder='Enter your verification code', required='')
            input.ghost-button(type='submit', value='Verify me!')

Check the verification code

To verify the code submitted by the user you make a verify check request. You pass in the request_id (which we stored in the /verify route handler) and the code provided.

The response from the Verify API check request tells you if the user entered the correct code. If the status is 0, log the user in by creating a user session object with a number property for display on the home page:

app.post('/check-code', (req, res) => {
    // Check the code provided by the user
    nexmo.verify.check({
        request_id: verifyRequestId,
        code: req.body.code
    }, (err, result) => {
        if (err) {
            console.error(err);
        } else {
            if (result.status == 0) {
                /* 
                    User provided correct code,
                    so create a session for that user
                */
                req.session.user = {
                    number: verifyRequestNumber
                }
            }
        }
        // Redirect to the home page
        res.redirect('/');
    });
});

Conclusion

You can now log a user into your web application without a password using just their phone number. To achieve this you collected their phone number, used the Verify API to send the user a code, collected this code from the user and sent it back to the Verify API to check.

Resources