使用 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 或终端中运行以下命令:
这个文件夹将作为您的 Express 项目文件夹。
接下来,运行以下命令安装 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 项目文件夹根目录下运行以下命令:
然后打开您常用的浏览器,访问 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 文件中找到以下代码行:
然后在这一行的正下方新增一行,添加以下代码:
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:
在 app,js 文件顶部添加以下代码,将 Axios 引入您的项目:
接下来,再次在您的 app.js 文件中找到以下这一行:
在这一行的正下方新增一行,添加以下代码:
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.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,请运行以下命令安装该包:
然后在文件顶部添加以下代码,将 express-session 引入您的 app.js:
最后,在 app.js 顶部(紧跟在 const app = express(); 下方)新增一行,添加以下代码,将 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,并以此作为接收找回验证码的渠道。在实际应用中,您可以根据用户可用的方式,改用邮件等其他渠道,甚至同时支持两者。