# 接入 MFA

多因素身份验证(MFA)是一种安全系统,是为了验证一项操作合法性而实行多种身份验证。例如银行的 U 盾,异地登录要求手机短信验证。阅读本教程后,你可以基于 Authing 的 MFA 功能进行定制化开发。

# 前期准备

  1. 注册一个 Authing 账号
  2. 创建一个用户池
  3. 创建一个 OIDC 应用
  4. 建议安装 http-server 用于快速启动 http 服务器来查看 html 文件。npm install -g http-server

# 所需技能

  1. 基础 HTML 知识
  2. 中级 Javascript 知识

# 新建一个空白 html 文件用于编写 Authing 程序

本教程只是为了演示,因此我们没选择高级框架,这可以让我们专注于 MFA 逻辑本身。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Authing MFA Example</title>
  </head>
  <body></body>
</html>

# 添加基本控件

为我们的 html 添加四个输入框和三个按钮。用于输入用户名、密码、验证码、MFA 口令等信息。我们使用 jquery 的插件来生成 MFA 令牌的二维码。

<div class="row">
  <label>用户名:<input id="username"/></label>
  <label>密码:<input id="password"/></label>
  <label>验证码:<input id="verify-code"/><img id="verify-img" style="vertical-align: bottom;"/></label>
</div>
<div id="mfa" style="visibility: hidden;" class="row">
  <label>MFA 口令:<input type="number" id="mfa-code"/></label>
</div>
<div class="row">
  <input type="button" value="登录" id="login" onclick="handleLogin()" />
  <input
    type="button"
    value="注册"
    id="register"
    onclick="handleRegister()"
  />
  <input
    type="button"
    value="开启 MFA"
    id="register"
    onclick="handleChangeMFA()"
  />
</div>
<div id="qrcode" class="row"></div>
<div id="qrcode-info" style="visibility: hidden;" class="row">
  请使用 MFA 令牌管理工具扫码添加令牌
</div>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/jquery.qrcode/1.0/jquery.qrcode.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/authing-js-sdk"></script>

# 初始化 SDK

浏览器端初始化时需传入用户池 ID

<script>
const auth = new Authing({
	userPoolId: 'your_userpool_id',
});

authing.login({...}).then(info => {})
authing.register({...}).then(info => {})

</script>

# 创建一个用户

将此函数绑定到登录按钮的点击事件上,用随机信息注册一个用户。

// 使用随机信息注册一个用户
async function handleRegister() {
  let rand = Math.random()
    .toString(36)
    .slice(2);
  let res = await validAuth.register({
    email: rand + "@test.com",
    password: "123456"
  });
  alert(JSON.stringify(res));
  document.getElementById("username").value = res.email;
  document.getElementById("password").value = "123456";
  window.userId = res._id;
}

# 开启指定用户的 MFA

操作用户的 MFA 开启状态之前,必须要先获得用户的 token。可以通过 validAuth.login 函数以用户的身份登录,或者直接使用用户的 token 初始化。

// 开启用户的 MFA
async function handleChangeMFA() {
  let username = document.getElementById("username").value;
  let password = document.getElementById("password").value;
  let verifyCode = document.getElementById("verify-code").value;

  try {
    let res = await validAuth.login({
      email: username,
      password: password,
      verifyCode: verifyCode
    });
    // 或者使用用户的 token 初始化
    // authing.initUserClient('用户的 JWT token');
    let mfa = await validAuth.changeMFA({
      userId: res._id,
      userPoolId: "5c95905578fce5000166f853",
      enable: true
    });
    alert(JSON.stringify(mfa));
    jQuery("#qrcode").qrcode(
      `otpauth://totp/Authing:Test?secret=${mfa.shareKey}&period=30&digits=6&issuer=Authing`
    );
    document.getElementById("qrcode-info").style.visibility = "visible";
  } catch (err) {
    alert("失败消息:" + JSON.stringify(err));
  }
}

# 生成 MFA 令牌二维码

社区有许多可以生成二维码的库,这里使用了 jquery 的二维码插件生成 MFA 二维码。

jQuery("#qrcode").qrcode(
  `otpauth://totp/Authing:Test?secret=${mfa.shareKey}&period=30&digits=6&issuer=Authing`
);

# 处理用户登录逻辑

当调用 validAuth.login 函数进行登录时,如果检测到用户开启了 MFA 二次验证,而没有携带 MFA 口令,会抛出一个错误,错误代码为 1635。在错误处理函数中,携带 MFA 口令再次调用 validAuth.login 即可。

// 登录逻辑
async function handleLogin() {
  // validAuth.login({email: })
  let username = document.getElementById("username").value;
  let password = document.getElementById("password").value;
  let verifyCode = document.getElementById("verify-code").value;
  if (isMFAShow) {
    let MFACode = document.getElementById("mfa-code").value;
    try {
      let res = await validAuth.login({
        email: username,
        password: password,
        MFACode: MFACode,
        verifyCode: verifyCode
      });
      alert("成功消息:" + JSON.stringify(res));
    } catch (err) {
      alert("失败消息:" + JSON.stringify(err));
      if(err.message.code === 2000 || err.message.code === 2001 || err.message.code === 2006) {
        console.log(err.message.data.url)
        document.getElementById("verify-img").src = err.message.data.url
      }
    }
  } else {
    try {
      let res = await validAuth.login({
        email: username,
        password: password,
        verifyCode: verifyCode
      });
      alert("成功消息:" + JSON.stringify(res));
    } catch (err) {
      alert("失败消息:" + JSON.stringify(err));

      if (err.message.code === 1635) {
        showMFAInput();
      }
      if(err.message.code === 2000 || err.message.code === 2001 || err.message.code === 2006) {
        console.log(err.message.data.url)
        document.getElementById("verify-img").src = err.message.data.url
      }
    }
  }
}

# 完整代码

Github: mfa-demo

# 运行方法

双击打开 index.html 文件。

或在项目目录启动一个 http 服务器。

$ npm install -g http-server
$ http-server

然后访问 127.0.0.1:8080。