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-id 和secret 。
//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-dbDataBase- 指定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-id 和secret-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-id 和temp-secret 来验证我们的注册用户。我们还需要将secret ,永久地设置到数据库中。
从数据库中检索ID和临时秘密
由于我们将需要user-id 和temp 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-id 和token 。
接下来,在Postman的body 部分,使用JSON数据发送检索到的user-id 和由验证器应用程序生成的相关token ,如下图所示。
{
"userId": "ed48c14e-cb85-4575-830c-c534d142f8e4",
"token":"127381"
}
应用程序的/verify 路由将使用下面的代码块从body 中提取token 和user-id 。
const {token, userId} = req.body;
接下来,我们将调用speakeasy的/verify 函数,通过检查token 和temp-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-id 和token 。
运行服务器
为了测试该应用程序,我们在index.js 文件中添加下面的代码块。
要运行服务器,请使用npm start 。
const PORT = process.env.PORT || 5000
app.listen(PORT, () =>{
console.log(`Server running on port ${PORT}`);
})
下面的图片显示了通过Postman发送的每个请求的结果。
注册的路线

验证路线

验证路线
总结
我们建立了一个Node.js应用程序,并使用Speakeasy为双因素认证编码了一个后端。我们还学习了如何使用chrome中的authenticator扩展来生成令牌。
最后,我们使用Postman来模拟从应用程序的前端发送请求。