Node.js中的Speakeasy双因素认证教程

774 阅读5分钟

Node.js中的Speakeasy双因素认证

双因素认证是一种在应用程序中实施的安全措施,通过为系统的授权提供两个独立的证据来提高安全性。双因素认证(2FA)的作用超越了用户名/电子邮件和密码认证。

在本教程中,我们将学习通过使用Speakeasy库来进行认证。我们还将学习使用Google Authenticator应用程序生成的令牌进行双因素认证的后端实现。

简介

实现2FA的方法之一是使用speakeasy库。

Speakeasy库使用一次性密码(OTP)提供双因素认证。该库为应用程序的标准认证过程提供了一个额外的安全层。

使用OTP,Speakeasy提供了账户访问所需的额外数据。

设置Node.js应用程序

让我们开始使用init 命令来初始化我们的应用程序。

npm init -y

该命令将创建一个package.json ,该文件保存了项目的元数据。

{
  "name": "node-two-factor-auth",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon start"
  },
  "keywords": [],
  "author": "mia-roberts",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.1",
    "node-json-db": "^1.3.0",
    "speakeasy": "^2.0.0",
    "uuid": "^8.3.2"
  }
}

安装依赖项

设置完成后,我们的应用程序需要一些特定的项目依赖。

这些依赖性包括。

  • express 作为后端服务器。
  • uuid 生成通用的唯一用户标识。
  • node-json-db 作为我们的数据库,将数据存储为JSON。
  • speakeasy 用于认证的库
  • nodemon 作为我们的开发依赖。

有了nodemon ,我们就不必在每次做出改变时重新启动我们的应用程序。

npm install –-save express, nodemon, speakeasy, uuid, node-json-db

设置应用程序

首先,我们将设置我们的服务器。

我们的服务器将在应用程序的入口点index.js 文件上运行。

因此,我们在index 文件下添加下面的代码块。

const express = require('express')
const app = express();
const PORT = process.env.PORT || 5000

app.use(express.json())

在设置好服务器后,我们把我们的依赖性引入到index.js 文件中。

uuid 将创建一个独特的用户ID,而node-json-db 将作为数据库来存储与该用户ID相关的user-idsecret

//adding the speakeasy library
const speakeasy = require('speakeasy')

//adding the uuid dependacy
const uuid = require('uuid')

//adding node-json-db
const { JsonDB } = require('node-json-db')
const { Config } = require('node-json-db/dist/lib/JsonDBConfig')

节点JSON数据库

我们的应用程序将使用node-json-db 模块来存储JSON格式的用户记录。

正如在node-json-db 的文档中提到的,为了初始化node-json-db ,我们将添加下面的脚本。

// instance of the node-json-db
const db = new JsonDB(new Config("DataBase", true, false, '/'));
  • new Config() - 创建一个 数据库配置。node-json-db
  • DataBase - 指定JSON存储文件的名称。
  • true - 告诉数据库在每次推送时都要保存。
  • false - 指示数据库以人类可读的格式保存数据库。
  • / - 在访问我们的数据库值时使用的分隔符。

使用Postman发送请求

由于我们的应用程序没有一个前端,我们将在向应用程序的后端发送请求时使用Postman。

Postman提供了一个处理请求的接口,否则这些请求将由HTML

postman ,我们将使用三个路由,/register/verify/validate 路由。

现在我们将创建如下的URLs。

  • 注册:http://localhost:5000/api/register
  • 验证:http://localhost:5000/api/verify
  • 验证:http://localhost:5000/api/validate

注册用户

在这个应用程序中,我们假设用户是用他/她的user-id 。因此,我们忽略了其他的用户识别细节。

我们将注册用户,并将他们的user-id 与speakeasy生成的secret-key 一起存储在Database.json 文件中。

注册过程开始于向index.js 文件中的/register 路线传递一个POST 请求。

然后我们使用uuid ,生成一个唯一的user-id ,并为用户ID生成一个临时的secret-key

接下来,我们将user-idsecret-key 存储在node-json-db

这个过程的代码如下。

app.post('/api/register', (request, response) =>{
    const userid = uuid.v4()
    try {
        const path = `user/${userid }` 
        const temp_secret = speakeasy.generateSecret()
        db.push(path,{ userid , temp_secret }) 
        // we only need the base32 
        response.json({userid , secret: temp_secret.base32})
    }catch (error) {
        console.log(error)
        response.status(500).json({message: 'Error generating the secret'})
    }
})

数据库中的用户对象将如下图所示。

{
    "user":{
        "00e296df-cff6-44ee-94f7-763de86962c3":{
            "id":"00e296df-cff6-44ee-94f7-763de86962c3",
            "temp_secret":{
                "ascii":"eez>9svVgNa$DE9TXZQw#z0dkXI!GSQT",
                "hex":"65657a3e39737656674e612444453954585a5177237a30646b58492147535154",
                "base32":"MVSXUPRZON3FMZ2OMESEIRJZKRMFUULXEN5DAZDLLBESCR2TKFKA",
                "otpauth_url":"otpauth://totp/SecretKey?secret=MVSXUPRZON3FMZ2OMESEIRJZKRMFUULXEN5DAZDLLBESCR2TKFKA"
            }
        }
    }
}

验证用户

接下来,我们需要使用他们的user-idtemp-secret 来验证我们的注册用户。我们还需要将secret ,永久地设置到数据库中。

从数据库中检索ID和临时秘密

由于我们将需要user-idtemp secret ,我们使用下面的代码从数据库中提取它们。

// Retrieve user from the database
const path = `/user/${userId}`;
const user = db.getData(path);

//destructure the base-32 of the temp=secret
const { base32: secret } = user.temp_secret;

生成验证令牌

接下来,我们使用上面的temp-secret ,使用验证器应用程序生成一个verification token

导航到Chrome ,在Extensions ,下载验证器。

我们将使用认证器为我们的用户生成验证令牌。

认证器将生成一个代码,我们将使用Postman提供给验证路线的JSON主体。

通过Postman发送验证响应的帖子请求

在Postman中,我们将为验证创建一个新的路由/verify ,在这里我们输入user-idtoken

接下来,在Postman的body 部分,使用JSON数据发送检索到的user-id 和由验证器应用程序生成的相关token ,如下图所示。

{
  "userId": "ed48c14e-cb85-4575-830c-c534d142f8e4",
  "token":"127381"
}

应用程序的/verify 路由将使用下面的代码块从body 中提取tokenuser-id

const {token, userId} = req.body; 

接下来,我们将调用speakeasy的/verify 函数,通过检查tokentemp-secret 来验证用户。

如果token ,该函数返回true ,如果 被成功验证。在返回true ,我们将数据库中的temp-secret 更新为permanent-secret

我们实现这个功能,如下所示。

const verified = speakeasy.totp.verify({
  secret,
  encoding: 'base32',
  token
});

if (verified) {
  // Update the temporary secret to a permanent secret
  db.push(path, { id: userId, secret: user.temp_secret });
  res.json({ verified: true })
} else {
  res.json({ verified: false})
}

验证令牌

我们需要不断验证来自认证器的令牌。

我们将创建一个名为/validate 的新路由。该路由将有相同的代码,除了一个时间窗口,之后将验证令牌。

此外,我们在这个阶段不会改变temp-secret

代码如下。

  //Validate the token
  app.post('/api/validate', (req,res) => {
    const {token, userId} = req.body;   
    try {
      // Retrieve user from database
      const path = `/user/${userId}`;
      const user = db.getData(path);
    
      const { base32: secret } = user.secret;
      const tokenValidate = speakeasy.totp.verify({
        secret,
        encoding: 'base32',
        token, 
        window:1 // time window
      });
    
      if (tokenValidate) {
        
        res.json({ validated: true })
      } else {
        res.json({ validated: false})
      }
    }catch(error) {
      console.error(error)
      res.status(500).json({ message: "Error retrieving user!"})
    };
  })

为了检查一个用户是否被验证,我们将在Postman中导航到/validate ,并提供user-idtoken

运行服务器

为了测试该应用程序,我们在index.js 文件中添加下面的代码块。

要运行服务器,请使用npm start

const PORT = process.env.PORT || 5000
app.listen(PORT, () =>{
    console.log(`Server running on port ${PORT}`);
})

下面的图片显示了通过Postman发送的每个请求的结果。Register route

注册的路线

Verify route

验证路线

Validate route

验证路线

总结

我们建立了一个Node.js应用程序,并使用Speakeasy为双因素认证编码了一个后端。我们还学习了如何使用chrome中的authenticator扩展来生成令牌。

最后,我们使用Postman来模拟从应用程序的前端发送请求。