DEV Community

Cover image for How to create QR based 2fa system in Node.js (step-by-step)
Jahongir Sobirov
Jahongir Sobirov

Posted on

How to create QR based 2fa system in Node.js (step-by-step)

Two-factor authentication (2FA) adds an extra layer of security to your apps. In this guide, we’ll use express and auth-verify, a Node.js library, to implement TOTP (Time-based One-Time Password) compatible with Google Authenticator.

We’ll cover:

  • Generating a TOTP secret
  • Creating a QR code for users
  • Verifying user-entered codes

Install

We should install all necessary packages from NPM.

npm install express path auth-verify
Enter fullscreen mode Exit fullscreen mode

File structure

Our file structure should be like this:

├── server.js
└── public/
    ├── index.html
Enter fullscreen mode Exit fullscreen mode

Import

We should setup all necessary packages to server.js.

const express = require("express");
const path = require("path");
const AuthVerify = require("auth-verify");

const app = express();
const PORT = 3000;

app.use(express.static(path.join(__dirname, 'public')));

const auth = new AuthVerify();

const secret = auth.totp.secret(); // generating a secret for totp

// Start server
app.listen(PORT, () => console.log(`Server running at http://localhost:${PORT}`));
Enter fullscreen mode Exit fullscreen mode

Make web page

We'll make index.html for showing QR code to user for getting TOTP codes.

<!DOCTYPE html>
<html>
<head>
  <title>Getting QR code for 2FA</title>
</head>
<body>
   <h1>Getting QR code for 2FA</h1>
  <button id="getQRBtn">Get QR</button><br>
  <img id="qrImage" /><br>
  <input type="text" id="userCode" placeholder="Enter your code">
  <button id="verifyBtn">Verify code</button><br>
  <span></span>

</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Backend part

Firstly we'll send our index.html file with GET route:

app.use(express.static(path.join(__dirname, 'public')));

app.get('/', (req, res)=>{
    res.sendFile(path.join(__dirname, 'index.html'));
});
Enter fullscreen mode Exit fullscreen mode

Secondly, we should generate TOTP code and QR data in server and we should send it to client.

app.get('/api/qr', async (req, res)=>{
    try {        
        // making otpauth URI for QR code
        const uri = auth.totp.uri({
            label: "user@example.com",
            issuer: "2FA system for dev.to",
            secret
        });

        // generating QR code
        const qr = await auth.totp.qr(uri);

        // We'll send this QR code to the client
        res.json({ qr });

    } catch(err){
        console.error(err);
        res.status(500).json({ error: 'Failed to generate QR' });
    }
});
Enter fullscreen mode Exit fullscreen mode

In step 3 we'll check user code whether is it correct or not:

app.post('/api/verify', async (req, res) => {
    const { code } = req.body; // we'll recieve code from client

    if (!code) { // for debugging...
        return res.status(400).json({ error: "No code provided" });
    }

    try {
        const verified = await auth.totp.verify(secret, code); // verifying user code
        res.json({ verified }); // and we'll send the result to the client
    } catch(err) {
        console.error(err);
        res.status(400).json({ error: "Verification failed", details: err.message });
    }
});
Enter fullscreen mode Exit fullscreen mode

Frontend part

Now we show user QR code to the web page and we should get code that was inserted by user

  <script src="https://cdn.jsdelivr.net/gh/jahongir2007/auth-verify/authverify.client.js"></script>
  <script>

  const qrImage = document.getElementById('qrImage');
  const qrBtn = document.getElementById('getQRBtn');
  const verifyBtn = document.getElementById('verifyBtn');
  const userCodeInput = document.getElementById('userCode');
  const resultSpan = document.querySelector('span');

  const auth = new AuthVerify({
    apiBase: 'http://localhost:3000',
    qrEl: qrImage
  });

  // GET QR code
  qrBtn.addEventListener('click', async () => {
    auth.get('/api/qr'); // set the URL for GET
    await auth.qr(); // fetch and display QR
  });

  // VERIFY code
  verifyBtn.addEventListener('click', async () => {
    const userCode = userCodeInput.value;

    auth.post('/api/verify'); // set URL for POST
    const result = await auth.verify(userCode); // send code to server

    if (result.verified) {
      resultSpan.innerText = '✅ Verified!';
    } else {
      resultSpan.innerText = '❌ Invalid code';
    }
  });

  </script>
Enter fullscreen mode Exit fullscreen mode

Result

Result

Full codes

index.html:

<!DOCTYPE html>
<html>
<head>
  <title>Getting QR code for 2FA</title>
</head>
<body>
  <h1>Getting QR code for 2FA</h1>
  <button id="getQRBtn">Get QR</button><br>
  <img id="qrImage" /><br>
  <input type="text" id="userCode" placeholder="Enter your code">
  <button id="verifyBtn">Verify code</button><br>
  <span></span>
  <script src="https://cdn.jsdelivr.net/gh/jahongir2007/auth-verify/authverify.client.js"></script>
  <script>

  const qrImage = document.getElementById('qrImage');
  const qrBtn = document.getElementById('getQRBtn');
  const verifyBtn = document.getElementById('verifyBtn');
  const userCodeInput = document.getElementById('userCode');
  const resultSpan = document.querySelector('span');

  const auth = new AuthVerify({
    apiBase: 'http://localhost:3000',
    qrEl: qrImage
  });

  // GET QR code
  qrBtn.addEventListener('click', async () => {
    auth.get('/api/qr'); // set the URL for GET
    await auth.qr(); // fetch and display QR
  });

  // VERIFY code
  verifyBtn.addEventListener('click', async () => {
    const userCode = userCodeInput.value;

    auth.post('/api/verify'); // set URL for POST
    const result = await auth.verify(userCode); // send code to server

    if (result.verified) {
      resultSpan.innerText = '✅ Verified!';
    } else {
      resultSpan.innerText = '❌ Invalid code';
    }
  });

  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

server.js:

const express = require('express');
const path = require('path');
const AuthVerify = require('auth-verify');

const app = express();
const PORT = 3000;

// Serve everything inside /public
app.use(express.json());

const auth = new AuthVerify();

const secret = auth.totp.secret(); // generating a secret

app.get('/api/qr', async (req, res)=>{
    try {        
        // making otpauth URI for QR code
        const uri = auth.totp.uri({
            label: "user@example.com",
            issuer: "2FA system for dev.to",
            secret
        });

        // generating QR code
        const qr = await auth.totp.qr(uri);

        // We'll send this QR code to the client
        res.json({ qr });

    } catch(err){
        console.error(err);
        res.status(500).json({ error: 'Failed to generate QR' });
    }
});

app.post('/api/verify', async (req, res) => {
    const { code } = req.body;

    if (!code) {
        return res.status(400).json({ error: "No code provided" });
    }

    try {
        const verified = await auth.totp.verify(secret, code);
        res.json({ verified });
    } catch(err) {
        console.error(err);
        res.status(400).json({ error: "Verification failed", details: err.message });
    }
});

app.use(express.static(path.join(__dirname, 'public')));

app.get('/', (req, res)=>{
    res.sendFile(path.join(__dirname, 'index.html'));
});

app.listen(PORT, ()=> console.log(`Server is runing at http://localhost:${PORT}`));
Enter fullscreen mode Exit fullscreen mode

I hope that would be useful for you

Top comments (0)