Two-factor authentication in Node.js – Stateless Application

September 19, 2017
by Rahil Shaikh
  • Two-factor authentication in stateless application

Two-factor authentication is nowadays quite popular for having an added layer of security in web applications. While this increases the steps to log in, it ensures that your account is secure and can only be accessed by the rightful owner.

If you have any account that is of High value to you, you must definitely enable Two-factor Authentication. Major software providers like Google, Amazon, etc. support two-factor authentication. In fact, there is a website called twofactorauth.org which lists down websites and shows if they support Two-factor authentication.

How does Two-factor Authentication works?

Normally, to log-in to an application you are required to provide a password. In the case of Two-factor authentication, you are also required to enter a temporary one-time password (sometimes referred as a token) in addition to the usual password. This OTP can be provided to you in various ways. The ways in which the OTP is delivered defines the different types of 2fa. The OTP can be provided via email, SMS, as a software token using apps like Google Authenticator or Authy, it can also come via Hardware tokens. OTPs generated via software and hardware token are usually referred as TOTP (Time-Based OTP). Hardware tokens work in a similar way to software token, the difference being that you have a dedicated piece of hardware solely responsible for generating tokens.

The Flow

  1. The user is asked to enter the username and password.
  2. The system verifies against the given credentials.
  3. On successful verification, the user is shown a screen to enter the OTP.
  4. The user enter’s the OTP which was either sent via email/sms or from the software/hardware token generator.
  5. The system verifies if the entered OTP is valid.
  6. On successful validation the system logs in the user.

This can be better understood with the below flowcharts.

Flow-chart

The State-less dilemma

In a truly stateless application, the system does not maintain any state of the user. Each and every request is treated individually, things like tokens are used to validate the user. This validation is done on every request.

This statelessness in a RESTful application contradicts with how two-factor authentication works. In 2fa you need to store some kind of an intermediate state of the user between the time when the user enters the username/password until she enters the OTP.

An Opinionated Approach

There are a few ways by which you can implement 2fa in a stateless RESTful application.

  1. Opaque Token: On the success of step1 send an opaque token to the client. The client must then return the token along with the OTP. The opaque token will help the system identify the user making the request.
  2. Resend Credentials: On step 1 verify the credentials. If successful the UI redirects the user to enter the OTP. Here the user only enters the OTP but the client makes sure that the user credentials are re-sent along with the OTP.

I prefer the second approach. Well, this is quite opinionated. Approach 1 would also work well but then you have to deal with all the hassles of setting appropriate validity of the opaque token, invalidating the token as soon as OTP verification is done.

Where as in the second approach you can deliver seamless experience to the user and also avoid too many complications in the implementation.

Our Application

Enough theory, now it’s time to get our hands dirty and try to implement this. We will build a small authentication application that has two-factor enabled.

Before we jump into implementation here are a few points to note.

  1. We will build our application in Node.js.
  2. ExpressJS will be our framework for building APIs.
  3. Implementing standard two-factor auth using SMS is quite straight forward, so we won’t cover that here. But I’ll mention the flow.
  4. We will add TOTP based two-factor authentication and use apps like Google Authenticator.
  5. We will use speakeasy npm package for the implementation of  TOTP.
  6. We will have an in-memory data-store. (For the demo, not recomended in production apps)

Structure

Nothing fancy here, two simple files, app.js for writing express APIs, vue.app.html for our front-end application and also a package.json you know for managing npm dependencies.

APIs

We will start with the backend of our application. Make sure your package.json looks like below.

package.json
{
  "name": "2fa-auth",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "keywords": [],
  "author": "Rahil Shaikh",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.18.0",
    "express": "^4.15.4",
    "qrcode": "^0.9.0",
    "speakeasy": "^2.0.0"
  }
}

Next, install the dependencies.

npm install

Now let’s setup a skeleton.

app.js
const express = require('express');
var app = express();
const bodyParser = require('body-parser');
var speakeasy = require('speakeasy');
var QRCode = require('qrcode'); //required for converting otp-url to dataUrl
const path = require('path');

app.use(bodyParser.json());

//EXPL: single in memory user
let user = {
    'firstName': "Jon",
    'lastName': "Doe",
    email: "[email protected]",
    password: "test"
}
//apis

app.listen('3000', ()=>{
    console.log('App running on 3000');
});

Just requiring a few modules and creating an express server. For demo purpose, we have an in-memory single user. We will use this user as an example. This is only to demonstrate the functionality, in a real application you will have multiple users stored in the database.

Let us now focus on building APIs. We will have 5 APIs. Obviously, we will need one login API. Then we will need two APIs to enable two-factor authentication for a user, one API to set it up and another to do a one-time verification. Next will also need an API to get two-factor setup details and finally one to disable two-factor authentication. Let’s add these, one by one.

app.js
//setup two factor for logged in user
app.post('/twofactor/setup', function(req, res){
    const secret = speakeasy.generateSecret({length: 10});
    QRCode.toDataURL(secret.otpauth_url, (err, data_url)=>{
        //save to logged in user.
        user.twofactor = {
            secret: "",
            tempSecret: secret.base32,
            dataURL: data_url,
            otpURL: secret.otpauth_url
        };
        return res.json({
            message: 'Verify OTP',
            tempSecret: secret.base32,
            dataURL: data_url,
            otpURL: secret.otpauth_url
        });
    });
});

This API will setup Two-factor for a logged in user. Note. we have not implemented session in this application, but in a real application, the respective user must be logged in.

TOTP is generated based on the combination of a secret key and current time. The secret key is usually a random base32 encoded string.
In the above API we are using speakeasy.generateSecret function to generate the secret key. Along with the secret key, the function also returns the otpauth_url, this can be used to generate a data_url which when added to an image tag will display the QRCode.
To get the dataURL we are using the qrcode module. Once we get the dataURL, we store the details with the logged in user and also send the same as the API’s response. Using the response of this API the client application can display the secret key and the QR code on the screen. The user will then scan the QR code using an app like google authenticator.

Note that we are storing the secret as tempSecret at the moment, that is because we do not want to enable two-factor auth for the user unless the user has verified by providing the token once.

Next, let’s add the verify API.

app.js
//setup two factor for logged in user
//..

//before enabling totp based 2fa; it's important to verify, so that we don't end up locking the user.
app.post('/twofactor/verify', function(req, res){
    var verified = speakeasy.totp.verify({
        secret: user.twofactor.tempSecret, //secret of the logged in user
        encoding: 'base32',
        token: req.body.token
    });
    if(verified){
        user.twofactor.secret = user.twofactor.tempSecret; //set secret, confirm 2fa
        return res.send('Two-factor auth enabled');
    }
    return res.status(400).send('Invalid token, verification failed');
});

This API takes in token as a body param. We use the verify method to check if the token is valid. We pass the tempSecret as the secret. If the token is valid, we set the secret key for the user thus enabling 2fa for the user.

We also need an API to disable 2fa, if in case the user does not wish to have it and also an API to get 2fa details, this is required to display the QRcode and secret key.

app.js
//setup two factor for logged in user
//..

//before enabling totp based 2fa; it's important to verify so that we don't end up locking the user.
//..

//get 2fa details
app.get('/twofactor/setup', function(req, res){
    res.json(user.twofactor);
});

//disable 2fa
app.delete('/twofactor/setup', function(req, res){
    delete user.twofactor;
    res.send('success');
});

 

Now finally we need to add the login API. Here we will accept the OTP in req headers. This is how it will work.
First, the login API must check if the user has enabled two-factor authentication. If not then the system must only check for credentials and authenticate the user. If 2fa is enabled, we again first check if the credentials are valid, this would be out 1st of the two steps in 2fa. If they are valid then the system will return a 206 status, which means partial success and also ask for the users OTP, upon the next request when the user enters the OTP, the client must also pass the credentials( which would be stored in the cache). Now since the credentials are valid and also the OTP is passed, we need to check if the OTP is valid or not. We do this obviously by using the verify() function. If the OTP is also valid, the system must authenticate the user.

app.js
//login API supports both, normal auth + 2fa
app.post('/login', function(req, res){
    if(!user.twofactor || !user.twofactor.secret){ //two factor is not enabled by the user
        //check credentials
        if(req.body.email == user.email && req.body.password == user.password){
            return res.send('success'); //authenticate user
        }
        return res.status(400).send('Invald email or password');
    } else {
        //two factor enabled
        if(req.body.email != user.email || req.body.password != user.password){
            return res.status(400).send('Invald email or password');
        }
        //check if otp is passed, if not then ask for OTP
        if(!req.headers['x-otp']){
            return res.status(206).send('Please enter otp to continue');
        }
        //validate otp
        var verified = speakeasy.totp.verify({
            secret: user.twofactor.secret,
            encoding: 'base32',
            token: req.headers['x-otp']
        });
        if(verified){ //authenticate user
            return res.send('success');
        } else { //Invalid otp
            return res.status(400).send('Invalid OTP');
        }
    }
});

I have added comments on every step, make sure you read them.

Now, remember we created a file named vue.app.html, that is where our front-end app will go. So we need to expose this file as a static path in express.

app.js
//EXPL: Front-end app
app.get('/', function(req, res){
    res.sendFile(path.join(__dirname+'/vue.app.html'));
});

That is it with our APIs, you can test them if required using a tool like Postman. Here is the complete app.js for your reference.

app.js
const express = require('express');
var app = express();
const bodyParser = require('body-parser');
var speakeasy = require('speakeasy');
var QRCode = require('qrcode');
const path = require('path');

app.use(bodyParser.json());

//EXPL: single in memory user
let user = {
    'firstName': "Jon",
    'lastName': "Doe",
    email: "[email protected]",
    password: "test"
}

//login API supports both, normal auth + 2fa
app.post('/login', function(req, res){
    if(!user.twofactor || !user.twofactor.secret){ //two factor is not enabled by the user
        //check credentials
        if(req.body.email == user.email && req.body.password == user.password){
            return res.send('success');
        }
        return res.status(400).send('Invald email or password');
    } else {
        //two factor enabled
        if(req.body.email != user.email || req.body.password != user.password){
            return res.status(400).send('Invald email or password');
        }
        //check if otp is passed, if not then ask for OTP
        if(!req.headers['x-otp']){
            return res.status(206).send('Please enter otp to continue');
        }
        //validate otp
        var verified = speakeasy.totp.verify({
            secret: user.twofactor.secret,
            encoding: 'base32',
            token: req.headers['x-otp']
        });
        if(verified){
            return res.send('success');
        } else {
            return res.status(400).send('Invalid OTP');
        }
    }
});

//setup two factor for logged in user
app.post('/twofactor/setup', function(req, res){
    const secret = speakeasy.generateSecret({length: 10});
    QRCode.toDataURL(secret.otpauth_url, (err, data_url)=>{
        //save to logged in user.
        user.twofactor = {
            secret: "",
            tempSecret: secret.base32,
            dataURL: data_url,
            otpURL: secret.otpauth_url
        };
        return res.json({
            message: 'Verify OTP',
            tempSecret: secret.base32,
            dataURL: data_url,
            otpURL: secret.otpauth_url
        });
    });
});

//get 2fa details
app.get('/twofactor/setup', function(req, res){
    res.json(user.twofactor);
});

//disable 2fa
app.delete('/twofactor/setup', function(req, res){
    delete user.twofactor;
    res.send('success');
});

//before enabling totp based 2fa; it's important to verify, so that we don't end up locking the user.
app.post('/twofactor/verify', function(req, res){
    var verified = speakeasy.totp.verify({
        secret: user.twofactor.tempSecret, //secret of the logged in user
        encoding: 'base32',
        token: req.body.token
    });
    if(verified){
        user.twofactor.secret = user.twofactor.tempSecret;
        return res.send('Two-factor auth enabled');
    }
    return res.status(400).send('Invalid token, verification failed');
});

//EXPL: Front-end app
app.get('/', function(req, res){
    res.sendFile(path.join(__dirname+'/vue.app.html'));
});

app.listen('3000', ()=>{
    console.log('App running on 3000');
});

Vue App

The vue.js app will again be for demo purposes, it will not necessarily follow best practices of building a vue.js application. Here we will have three components/screens, let’s break them down first before we have a look at the complete file.

Vue App
const routes = [
            { path: '/login', component: Login },
            { path: '/otp', component: Otp },
            { path: '/setup', component: Setup }
        ];
        const router = new VueRouter({
            routes // short for `routes: routes`
        });
        const app = new Vue({
            router
        }).$mount('#app');

As you can see above, we are mounting our Vue.js app and we have 3 components, let us have a look at them each, shall we?

Login Component
Login Component
const Login = {
            template: `
                    <div class="col-md-4 col-md-offset-4">
                        <form>
                            <div class="form-group">
                                <label for="email">Email address:</label>
                                <input v-model="email" type="email" class="form-control" id="email">
                            </div>
                            <div class="form-group">
                                <label for="pwd">Password:</label>
                                <input v-model="password" type="password" class="form-control" id="pwd">
                            </div>
                            <div class="checkbox">
                                <label><input type="checkbox"> Remember me</label>
                            </div>
                            <button v-on:click="login(email, password)"  class="btn btn-default">Submit</button>
                        </form>
                    </div>
            `,
            methods: {
                login: function(email, password){
                    localStorage.email = email;
                    localStorage.password = password;
                    this.$http.post('/login', { email: email, password: password}).then( response =>{
                        if(response.status === 206){
                            return router.push('otp');
                        } else if(response.status === 200) {
                            localStorage.clear();
                            localStorage.loggedin = true;
                            return router.push('setup');
                        }
                    }).catch(err => {
                        alert("Invalid creds");
                    });
                }
            },
            data: function(){
                return {
                    email: "[email protected]",
                    password: "test"
                }
            }
         }

In our Login we are taking in email and password as input and logging in the user. Now, depending on if 2fa is enabled or not we redirect the user to the right screen. So if 2fa is enabled the user must be redirected to enter the OTP (which is our OTP component), else the user will be logged in to the system.

OTP Component
OTP Component
const Otp = {
            template: `
                <div class="col-md-4 col-md-offset-4">
                    <form>
                        <div class="form-group">
                            <label for="otp">Enter Otp:</label>
                            <input v-model="otp" type="otp" class="form-control" id="otp">
                        </div>
                        <button v-on:click="login(otp)"  class="btn btn-default">Submit</button>
                    </form>
                </div>
            ` ,
            data: function(){
                return {
                    otp: ""
                }
            },
            methods: {
                login: function(otp){
                    const options = {
                        headers: {
                            ['x-otp']: otp
                        }
                    }
                    const payload = {
                        email: localStorage.email,
                        password: localStorage.password
                    }
                    this.$http.post('/login', payload, options).then((response)=>{
                        if(response.status === 200){
                            localStorage.clear();
                            localStorage.loggedin = true;
                            return router.push('setup');
                        }
                        alert('Invalid creds');
                    }).catch(err => {
                        alert("Invalid creds");
                    });
                }
            }
        }

Here, in our OTP component we have a field to allow the user to enter the OTP. Note that, on the click of the submit button we are calling the login API, but along with the OTP in the header as x-otp we are also passing the users credentials (email, password) in the request body. If the API returns a positive response we must log in the user.

Setup Component
Setup Component
const Setup = {
            template: `
                <div>
                    <div class="col-md-4 col-md-offset-4" v-if="twofactor.secret">
                        <h3>Current Settings</h3>
                        <img :src="twofactor.dataURL" alt="..." class="img-thumbnail">
                        <p>Secret - {{twofactor.secret || twofactor.tempSecret}}</p>
                        <p>Type - TOTP</p>
                    </div>
                    <div class="col-md-4 col-md-offset-4" v-if="!twofactor.secret">
                        <h3>Setup Otp</h3>
                        <div>
                            <button v-on:click="setup()"  class="btn btn-default">Enable</button>
                        </div>
                        <span v-if="!!twofactor.tempSecret">
                            <p>Scan the QR code or enter the secret in Google Authenticator</p>
                            <img :src="twofactor.dataURL" alt="..." class="img-thumbnail">
                            <p>Secret - {{twofactor.tempSecret}}</p>
                            <p>Type - TOTP</p>
                            <form>
                                <div class="form-group">
                                    <label for="otp">Enter Otp:</label>
                                    <input v-model="otp" type="otp" class="form-control" id="otp">
                                </div>
                                <button v-on:click="confirm(otp)"  class="btn btn-default">confirm</button>
                            </form>
                        </span>
                    </div>
                    <div class="col-md-1">
                        <h3>Disable</h3>
                        <form>
                            <button v-on:click="disable()"  class="btn btn-danger">Disable</button>
                        </form>
                    </div>
                </div>
            `,
            methods: {
                /** setup two factor authentication*/
                setup: function(){
                    this.$http.post('/twofactor/setup', {}).then(response => {
                        const result =  response.body;
                        if(response.status === 200){
                            console.log(result);
                            alert(result.message);
                            this.twofactor = result;
                        }
                    });
                },
                /** Verify the otp once to enable 2fa*/
                confirm: function(otp){
                    const body = {
                        token: otp
                    }
                    this.$http.post('/twofactor/verify', body).then(response => {
                        const result =  response.body;
                        if(response.status === 200){
                            this.twofactor.secret = this.twofactor.tempSecret;
                            this.twofactor.tempSecret = "";
                        }
                    }).catch(err=>alert('invalid otp'));
                },
                /** disable 2fa */
                disable: function(){
                    this.$http.delete('/twofactor/setup').then(response => {
                        const result =  response.body;
                        if(response.status === 200){
                            router.push('login');
                        }
                    }).catch(err => alert('error occured'));
                }
            },
            data: function(){
                return {
                    twofactor: {
                        secret: "",
                        tempSecret: ""
                    },
                    otp: ""
                }
            },
            /** when component is created check if 2fa is enabled*/
            created: function(){
                this.$http.get('/twofactor/setup').then(response => {
                    const result =  response.body;
                    if(response.status === 200 && !!result.secret){
                        this.twofactor = result
                    }
                }).catch((err)=>{
                    if(err.status === 401){
                        router.push('login');
                    }
                });
            }
        }

In the setup component, for our demo we basically allow the user to enable/disable two-factor authentication.

Complete File

This is how our complete vue.app.html file looks like.

vue.app.html
<!DOCTYPE>
<html>
    <head>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" type="text/css">
    </head>
    <body>
        <div id="app" class="container">
            <div class="row">
                <h1>WELCOME</h1>
                <p>
                    <router-link to="/login">Go to Login</router-link>
                </p>
                <router-view></router-view>
            </div>            
        </div>
    </body>
    <script src="https://unpkg.com/vue"></script>
    <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
    <script>
        const NotFound = { template: '<p>Page not found</p>' }
        const Login = {
            template: `
                    <div class="col-md-4 col-md-offset-4">
                        <form>
                            <div class="form-group">
                                <label for="email">Email address:</label>
                                <input v-model="email" type="email" class="form-control" id="email">
                            </div>
                            <div class="form-group">
                                <label for="pwd">Password:</label>
                                <input v-model="password" type="password" class="form-control" id="pwd">
                            </div>
                            <div class="checkbox">
                                <label><input type="checkbox"> Remember me</label>
                            </div>
                            <button v-on:click="login(email, password)"  class="btn btn-default">Submit</button>
                        </form>
                    </div>
            `,
            methods: {
                login: function(email, password){
                    localStorage.email = email;
                    localStorage.password = password;
                    this.$http.post('/login', { email: email, password: password}).then( response =>{
                        if(response.status === 206){
                            return router.push('otp');
                        } else if(response.status === 200) {
                            localStorage.clear();
                            localStorage.loggedin = true;
                            return router.push('setup');
                        }
                    }).catch(err => {
                        alert("Invalid creds");
                    });
                }
            },
            data: function(){
                return {
                    email: "[email protected]",
                    password: "test"
                }
            }
         }
        const Otp = {
            template: `
                <div class="col-md-4 col-md-offset-4">
                    <form>
                        <div class="form-group">
                            <label for="otp">Enter Otp:</label>
                            <input v-model="otp" type="otp" class="form-control" id="otp">
                        </div>
                        <button v-on:click="login(otp)"  class="btn btn-default">Submit</button>
                    </form>
                </div>
            ` ,
            data: function(){
                return {
                    otp: ""
                }
            },
            methods: {
                login: function(otp){
                    const options = {
                        headers: {
                            ['x-otp']: otp
                        }
                    }
                    const payload = {
                        email: localStorage.email,
                        password: localStorage.password
                    }
                    this.$http.post('/login', payload, options).then((response)=>{
                        if(response.status === 200){
                            localStorage.clear();
                            localStorage.loggedin = true;
                            return router.push('setup');
                        }
                        alert('Invalid creds');
                    }).catch(err => {
                        alert("Invalid creds");
                    });
                }
            }
        }
        const Setup = {
            template: `
                <div>
                    <div class="col-md-4 col-md-offset-4" v-if="twofactor.secret">
                        <h3>Current Settings</h3>
                        <img :src="twofactor.dataURL" alt="..." class="img-thumbnail">
                        <p>Secret - {{twofactor.secret || twofactor.tempSecret}}</p>
                        <p>Type - TOTP</p>
                    </div>
                    <div class="col-md-4 col-md-offset-4" v-if="!twofactor.secret">
                        <h3>Setup Otp</h3>
                        <div>
                            <button v-on:click="setup()"  class="btn btn-default">Enable</button>
                        </div>
                        <span v-if="!!twofactor.tempSecret">
                            <p>Scan the QR code or enter the secret in Google Authenticator</p>
                            <img :src="twofactor.dataURL" alt="..." class="img-thumbnail">
                            <p>Secret - {{twofactor.tempSecret}}</p>
                            <p>Type - TOTP</p>
                            <form>
                                <div class="form-group">
                                    <label for="otp">Enter Otp:</label>
                                    <input v-model="otp" type="otp" class="form-control" id="otp">
                                </div>
                                <button v-on:click="confirm(otp)"  class="btn btn-default">confirm</button>
                            </form>
                        </span>
                    </div>
                    <div class="col-md-1">
                        <h3>Disable</h3>
                        <form>
                            <button v-on:click="disable()"  class="btn btn-danger">Disable</button>
                        </form>
                    </div>
                </div>
            `,
            methods: {
                /** setup two factor authentication*/
                setup: function(){
                    this.$http.post('/twofactor/setup', {}).then(response => {
                        const result =  response.body;
                        if(response.status === 200){
                            console.log(result);
                            alert(result.message);
                            this.twofactor = result;
                        }
                    });
                },
                /** Verify the otp once to enable 2fa*/
                confirm: function(otp){
                    const body = {
                        token: otp
                    }
                    this.$http.post('/twofactor/verify', body).then(response => {
                        const result =  response.body;
                        if(response.status === 200){
                            this.twofactor.secret = this.twofactor.tempSecret;
                            this.twofactor.tempSecret = "";
                        }
                    }).catch(err=>alert('invalid otp'));
                },
                /** disable 2fa */
                disable: function(){
                    this.$http.delete('/twofactor/setup').then(response => {
                        const result =  response.body;
                        if(response.status === 200){
                            router.push('login');
                        }
                    }).catch(err => alert('error occured'));
                }
            },
            data: function(){
                return {
                    twofactor: {
                        secret: "",
                        tempSecret: ""
                    },
                    otp: ""
                }
            },
            /** when component is created check if 2fa is enabled*/
            created: function(){
                this.$http.get('/twofactor/setup').then(response => {
                    const result =  response.body;
                    if(response.status === 200 && !!result.secret){
                       this.twofactor = result
                   }
               }).catch((err)=>{
                   if(err.status === 401){
                       router.push('login');
                    }
                });
            }
        }
       
        const routes = [
            { path: '/login', component: Login },
            { path: '/otp', component: Otp },
            { path: '/setup', component: Setup }
        ];
        const router = new VueRouter({
            routes // short for `routes: routes`
        });
        const app = new Vue({
            router
        }).$mount('#app');
    </script>
</html>

Run the Application

Done with coding, now is the time to try our app out. Run the app.

node app.js

This should start our app on port 3000. Navigate to localhost:3000 in any browser. Click on go to Login link. You should see a login form with pre populated data.

Login

Since two-factor authentication is not yet enabled, the user will get logged in to the system. Click on the Setup button to initiate setup. This will call the POST /towfactor/setup API we made earlier.

On successful response you will be asked to scan the QR code using a mobile App like Google Authenticator, alternatively, you can also enter the secret key.

setup

Once you scan this QRcode, Google Authenticator will start generating Time-based One Time Passwords. To completely enable 2fa, enter the token and click confirm. This will call our POST /towfactor/verify API to verify and enable Two-factor authentication for that user.

On successful verification, 2fa will be enabled. The current setting will be displayed on the setup page.

Now let’s head back to the login page and try to log-in again. Click on Go-to Login link.

Now once you hit login, the system will ask you to enter the OTP. Enter the token displayed by your Google Authenticator app.

Token

Now only if you enter the correct OTP, will the system allow you to proceed to the setup page.

Code Base

You can download or access the complete code base here.

Conclusion

Thus we have learned how to implement two-factor authentication in a Node.js application. Here, I have also presented an opinionated approach to implement Two-factor authentication in a Stateless application, would love to hear your take on it.

Although we learned two-factor implementation, in a real application things would be a bit different, like the users would be in database and session/tokens should be handled on successful login. But that is completely up to the application developer to implement and is out of the scope of this article.

 

About

Engineer. Blogger. Thinker. Loves programming and working with emerging tech. We can also talk on Football, Gaming, World Politics, Monetary Systems.

Free PDF

Subscribe and get AngularJS Tips PDF. We never spam!
First Name:
Email:

2 comments

  1. Arek
    |

    Hi, Super article, could you please say what theme are you using it looks nice and reading is easy? Can I find this theme for vsc?

    • |

      Thanks, Arek.

      This is a customised theme built on top of Samba WordPress theme.

Leave a Comment