跳转至

使用 Authentication Flow API 实现自定义账户找回界面

在本指南中,您将学习如何借助 Authentication Flow API 实现属于您自己的自定义账户找回页面。

在开始之前,先来看看本文的目标概览:

我们将构建什么

在本指南结束时,我们将使用 Express.js 构建一个自定义账户找回界面,实现以下功能:

  • 允许用户输入与其账号关联的手机号作为登录 ID。
  • 向该手机号发送账户找回验证码。
  • 提供一个界面供用户输入并验证找回验证码。
  • 最后,提供一个界面让用户为账号设置新密码。

前置条件

要完成本指南,您需要具备以下条件:

  • 本地机器已安装 Node.js。
  • 一款代码编辑器,如 VS Code、Sublime、Atom,或您平时用于 JavaScript 开发的任意编辑器。
  • 熟悉 NPM 等命令行工具的使用。
  • 最后但同样重要的是,一个 Authany 账号。点此免费注册。

第 1 步:将您的 Authany 项目设置为使用自定义界面

在 Authany 应用中使用自定义界面,需要将 Custom UI URI 的值设置为您的自定义界面所在的地址。

要设置该值,请登录 Authany 管理台,进入应用并选择您的应用。接下来,在该应用的配置页面中,滚动到 Custom UI 部分,输入您的自定义界面的 URL。

在本文的示例中,我们将使用 CloudFlare Tunnel 把我们即将构建的自定义界面暴露出来,这样就能得到一个公开 URL,填入 Authany 应用配置中的 Custom UI URI 字段。

如果您还不熟悉 Authentication Flow API,可以先阅读这篇入门文章 [HERE(LINK)],了解如何为该 API 配置您的项目。

第 2 步:创建 Express 项目

现在让我们创建将用于实现账户找回自定义界面的 Express 项目。

要创建新的 Express 项目,请在您的计算机上使用文件资源管理器新建一个名为 "custom-recovery" 的文件夹,或者在 PowerShell 或终端中运行以下命令:

mkdir custom-recovery

这个文件夹将作为您的 Express 项目文件夹。

接下来,运行以下命令安装 Express 包:

npm i express

最后,在刚才新建的文件夹中创建一个名为 "app.js" 的新 JavaScript 文件。我们将在这个文件中实现应用逻辑。

将以下代码添加到新的 app.js 文件中:

const express = require('express');

const app = express();
const port = process.env.PORT || 3000;

app.get('/', (req, res) => {
    res.send(`
    <p>A demo for account recovery using authflow API. Click on the following link to try it</p>
    <a href="/recovery">Forgot password</a>
    `)

});

app.get('/recovery', async (req, res)=> {
    res.send("Test recovery");
});

app.listen(port, () => {
    console.log(`server started on port ${port}!`);
});

到这里,先测试一下目前的成果:在 Express 项目文件夹根目录下运行以下命令:

node app.js

然后打开您常用的浏览器,访问 http://localhost:3000/。

第 3 步:创建账户找回页面

这一步我们将为找回流程的第一个页面创建界面。这是一个基础网页,包含一个供用户输入手机号(登录 ID)的输入框和一个提交按钮。

在代码编辑器中打开 app.js,将 app.get('/recovery') 路由的内容更新为:

app.get('/recovery', async (req, res)=> {
    const URLQuery = rawURLQuery(req.url);
    res.send(`
    <!DOCTYPE html>
    <html lang="en">

    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css"
            integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
        <title>Account recovery</title>
    </head>

    <body>
        <div class="container pt-4">
            <form class="" action="./recovery" method="POST" enctype="application/x-www-form-urlencoded">
                <div class="">
                    <label class="">
                        Phone number (include country code)
                    </label>
                    <input name="phone" id="phone" type="text" class="form-control mb-2" placeholder="Enter your phone number" />
                </div>
                <input type="hidden" name="state_token" value="${await initRecovery(URLQuery)}">
                <button type="submit" class="btn btn-primary">
                    Submit
                </button>
            </form>
        </div>
    </body>
    </html>
    `);
});

上面的代码中用到了尚未实现的 rawURLQuery() 和 initRecovery() 函数。我们将在本步骤中实现第一个函数,在下一步实现第二个函数。

用上面的代码创建出的表单如下所示:

在您的 app.js 文件中找到以下代码行:

const port = process.env.PORT || 3000;

然后在这一行的正下方新增一行,添加以下代码:

function rawURLQuery(url) {
    const index = url.indexOf('?');
    return (index === 0) ? url.substr(index + 1) : "";
}

上面的代码实现了我们之前提到的 rawURLQuery() 函数。这个辅助函数用于从当前请求中提取 URL 查询参数。我们需要从 Authany 返回的一个特殊 URL 查询参数中,获取找回流程结束时的 finish redirect URI。您可以在 [HERE(LINK)] 了解更多关于该 URL 查询参数的内容。

第 3 步:初始化找回流程

在这一步中,我们将初始化一个类型为 account_recovery 的新身份验证流程。

在这一步以及后续步骤中,我们都会使用 Axios node 包向 Authentication Flow API 发起 HTTP 请求。运行以下命令安装 Axios:

npm i axios

在 app,js 文件顶部添加以下代码,将 Axios 引入您的项目:

const axios = require('axios');

接下来,再次在您的 app.js 文件中找到以下这一行:

const port = process.env.PORT || 3000;

在这一行的正下方新增一行,添加以下代码:

const endpoint = "https://cube-crisp-110.authanyid.com";
async function initRecovery(url_query) {

    const url = `${endpoint}/api/v1/authentication_flows?${url_query}`;

    const input = {
        "type": "account_recovery",
        "name": "default"
    };

    const headers = {
        "Content-Type": "application/json",
        "Accept": "application/json",
    }

    try {
        startRecovery = await axios.post(url, input, {
            headers: headers
        });

        return startRecovery.data.result.state_token;
    }
    catch (error) {
        console.log(error.response.data.error);
        return error.response;
    }
}

上面的代码实现了 initRecovery() 函数。该函数发送 HTTP 请求以初始化一个新的账户找回流程,并返回一个 state_token。我们会用这个 state_token 继续流程的下一步。因此,我们通过 <input type="hidden"> 将 state_token 的值传递到下一步。

该 HTTP 请求的响应如下所示:

{
    "result": {
        "state_token": "authflowstate_5QKK3BRPETYQ7SYWYFQ4N2MR3F0S8S0Q",
        "type": "account_recovery",
        "name": "default",
        "action": {
            "type": "identify",
            "data": {
                "options": [
                    {
                        "identification": "email"
                    },
                    {
                        "identification": "phone"
                    }
                ]
            }
        }
    }
}

第 4 步:发送找回验证码

在这一步中,我们将实现这样一段代码:当用户在表单中输入手机号并点击提交后,向该手机号发送找回验证码。

在 app.js 中(紧跟在 initRecovery() 函数下方)添加一个新的 beginRecovery() 函数,代码如下:

async function beginRecovery(login_id, state_token) {

    const url = `${endpoint}/api/v1/authentication_flows/states/input`;

    const input = {
        "state_token": state_token,
        "batch_input": [
            {
                "identification": "phone",
                "login_id": login_id
            },
            {
                "index": 0 //option of the channel the recovery code will be sent to
            }
        ]
    };

    const headers = {
        "Content-Type": "application/json",
        "Accept": "application/json",
    }

    try {
        const recoveryIdentityStep = await axios.post(url, input, {
            headers: headers
        });

        return recoveryIdentityStep;
    }
    catch (error) {
        console.log(error.response.data.error);
        return error.response;
    }
}

上面的代码会发送一个请求,其中包含用户的手机号(login_id)以及验证码应发送到的渠道位置(index)。

为了让 Express 能够正确处理表单数据,请在 app.js 顶部(紧跟在 const app = express(); 下方)新增一行,添加以下代码:

app.use(express.urlencoded({ extended: true }));

现在,紧跟在 app.get('/recovery') 路由下方,添加以下代码创建一个 POST 路由,用于调用 beginRecovery() 函数:

app.post('/recovery', async (req, res) => {
    try {
        const apiResponse = await beginRecovery(req.body.phone, req.body.state_token);
        if (apiResponse.status == 200) {
            req.session.recovery_response = apiResponse.data.result.action.data;
            req.session.recovery_response.state_token = apiResponse.data.result.state_token;
            res.redirect("/verifyRecovery");
        } else {
            res.send("Error!")
        }
    }
    catch (error) {
        console.log(error)
        res.send("Error: authentication failed!");
    }
});

如果找回验证码发送成功,上面的代码会将用户重定向到 verifyRecovery 页面。我们将在下一步实现这个页面。

上面的代码用到了 express-session,请运行以下命令安装该包:

npm i express-session

然后在文件顶部添加以下代码,将 express-session 引入您的 app.js:

const session = require('express-session');

最后,在 app.js 顶部(紧跟在 const app = express(); 下方)新增一行,添加以下代码,将 express-session 作为中间件启用:

const session = require('express-session');

第 5 步:验证找回验证码

现在让我们来实现这样一个页面:用户可以在此输入他们收到的用于验证的找回验证码。

在 app.js 中添加一个新路由,为验证页面创建界面:

app.get('/verifyRecovery', async (req, res) => {
    const recoveryResponseData = req.session.recovery_response;
    res.send(`
    <!DOCTYPE html>
    <html lang="en">

    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css"
            integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
        <title>Verify recovery code</title>
    </head>

    <body>
        <div class="container pt-4">
            <form class="" action="./verifyRecovery" method="POST" enctype="application/x-www-form-urlencoded">
                <div class="">
                <p>Email: ${recoveryResponseData.masked_display_name}</p>
                    <label class="">
                        Code
                    </label>
                    <input name="code" id="code" type="text" class="form-control mb-2" placeholder="Enter the Recovery code (000000 for this test) sent to the above email " />
                </div>
                <input type="hidden" name="state_token" value="${recoveryResponseData.state_token}">
                <button type="submit" class="btn btn-primary">
                    Verify
                </button>

                <div>
                    <a href="/resendOtp">Resend Code</a>
                    <span> (wait until ${recoveryResponseData.can_resend_at})</span>
                </div>
            </form>
        </div>
    </body>
    </html>
    `);
});

验证找回验证码页面如下所示:

接下来,用以下代码在 app.js 中添加一个新的 verifyRecoveryCode() 函数:

async function verifyRecoveryCode(code, state_token) {

    const url = `${endpoint}/api/v1/authentication_flows/states/input`;

    const input = {
        "state_token": state_token,
        "input": {
            "account_recovery_code": code
        }
    };

    const headers = {
        "Content-Type": "application/json",
        "Accept": "application/json",
    }

    try {
        const verifyCodeStep = await axios.post(url, input, {
            headers: headers
        });

        return verifyCodeStep;
    }
    catch (error) {
        return error.response;
    }
}

上面的代码会将用户输入的账户找回验证码,通过请求发送给 Authentication Flow API。

为完成找回验证码的验证流程,添加一个新路由来处理表单提交,代码如下:

app.post('/verifyRecovery', async (req, res) => {
    try {
        const apiResponse = await verifyRecoveryCode(req.body.code, req.body.state_token);
        if (apiResponse.status == 200) {
            req.session.verify_step_state_token = apiResponse.data.result.state_token;
            res.redirect("/setPassword");
        } else {
            res.send("Error!")
        }
    }
    catch (error) {
        console.log(error)
        res.send("Error: authentication failed!");
    }
});

在验证码验证成功后,上面的示例代码会将用户重定向到 setPassword 页面。在下一步中,我们将实现让用户设置新密码的页面。

第 6 步:实现设置新密码页面

账户找回流程的最后一步,是让用户为账号设置新密码。在这一步中,我们将实现相应的界面。

首先,在您的 app.js 中添加一个新路由,用以下代码创建该界面:

app.get('/setPassword', async (req, res) => {
    res.send(`
    <!DOCTYPE html>
    <html lang="en">

    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css"
            integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
        <title>Set new password</title>
    </head>

    <body>
        <div class="container pt-4">
            <form class="" action="./setPassword" method="POST" enctype="application/x-www-form-urlencoded">
                <div class="">
                <p>Set new password</p>
                </div>
                <div class="form-group">
                    <label>
                        New Password
                    </label>
                    <input name="password" type="password" class="form-control mb-2" placeholder="Enter your password" />
                </div>
                <div class="form-group">
                    <label>
                        Repeat Password
                    </label>
                    <input name="password2" type="password" class="form-control mb-2" placeholder="Enter your password" />
                </div>
                <input type="hidden" name="state_token" value="${req.session.verify_step_state_token}">
                <button type="submit" class="btn btn-primary">
                    Submit
                </button>
            </form>
        </div>
    </body>
    </html>
    `);
});

上面的代码将实现如下所示的页面:

用以下代码在 app.js 中添加一个新的 recoverySetPassword() 函数:

async function recoverySetPassword(password, state_token) {
    const url = `${endpoint}/api/v1/authentication_flows/states/input`;

    const input = {
        "state_token": state_token,
        "input": {
            "new_password": password
        }
    };

    const headers = {
        "Content-Type": "application/json",
        "Accept": "application/json",
    }

    try {
        const verifyCodeStep = await axios.post(url, input, {
            headers: headers
        });

        return verifyCodeStep;
    }
    catch (error) {
        console.log(error.response.data.error);
        return error.response;
    }
}

接下来,创建用于处理设置新密码表单提交的路由,代码如下:

app.post('/setPassword', async (req, res) => {
    try {
        const apiResponse = await recoverySetPassword(req.body.password, req.body.state_token);
        if (apiResponse.status == 200 && apiResponse.data.result.action.data.finish_redirect_uri !== undefined) {
            //password reset complete, return control back to Authgear
            res.redirect(apiResponse.data.result.action.data.finish_redirect_uri);
        } else {
            res.send(apiResponse.data)
        }
    }
    catch (error) {
        console.log(error)
        res.send("Error: account recovery failed!");
    }
});

如果新密码设置成功,上面的代码会将控制权交还给 Authany,以完成身份验证流程的剩余部分并返回到您的应用。这是通过将客户端重定向到 finish_redirect_uri 实现的。

结语

到这里,您已经成功借助 Authentication Flow API 实现了属于自己的自定义密码找回界面。

在我们的示例应用中,我们使用手机号作为 login_id,并以此作为接收找回验证码的渠道。在实际应用中,您可以根据用户可用的方式,改用邮件等其他渠道,甚至同时支持两者。