背景:
我有一个 React Web 应用程序(正在使用aws-amplify
),它连接到/使用 AWS Cognito 用户池进行身份验证。
我正在尝试启用 MFA,更具体地说,我希望我的用户可以选择使用软件令牌 TOTP MFA(即 Google Authenticator 或类似应用程序)。
当我将我的用户池设置为需要 MFA 时,我被迫启用 SMS MFA,然后软件 TOTP 是可选的。就我而言,我启用了 TOTP。
在我的网络应用程序中,我通过以下方式添加了必要的组件:
import { SelectMFAType } from 'aws-amplify-react';
# other code
<SelectMFAType authData={user} MFATypes={{ SMS: true, TOTP: true }} />
# other code
如果您不熟悉aws-amplify-react
and/or SelectMFAType
,此组件提供了一个 UI 元素,用户可以在其中选择是否喜欢使用 SMS 或软件 TOTP 作为其 MFA 方法。如果他们选择 SMS,则使用他们之前验证的电话号码,一切正常。
如果用户选择 TOTP,他们会在他们选择的身份验证器应用程序中显示一个 QR 码以进行扫描,并提示他们输入字段以输入来自身份验证器应用程序的 6 位数字以验证 TOTP。对于在任何其他网站上使用 TOTP MFA 选项的任何人来说,这都是非常标准的。如果用户从应用程序输入正确的代码,他们的 TOTP 选择就会得到验证。
简而言之SelectMFAType
,它只是快速原型和测试的捷径/替身,无需创建自定义组件。
问题:
现在,这是问题以及如何重现它。(起点是刚刚启用 TOTP 的用户。):
- 用户注销。
- 用户登录。
- 如果用户名/密码正确,则提示用户输入 TOTP。
- 如果 TOTP 正确,则用户已登录。到目前为止,这一切正常,但不会一直如此。
- 用户注销。
- 用户登录。
- 如果用户名/密码正确,系统将提示用户输入 SMS MFA,并将收到一条带有 6 位代码的短信。这是意外的行为。我希望它会继续从他们的身份验证器应用程序请求 TOTP MFA,除非用户将他们的首选方法更改回 SMS。
- 从现在开始,用户将只会被要求 SMS MFA。
在步骤 4 和步骤 7 之间,用户的偏好没有改变。React 应用程序或 AWS 用户池设置中绝对没有任何变化。
此外,如果我通过 AWS CLI 命令询问用户
aws cognito-idp admin-get-user --user-pool-id ${MY_ID} --username ${MY_USER_NAME}
,我可以确认用户的 MFA 首选项正是我所期望的:
{
<other irrelevant keys redacted>
"PreferredMfaSetting": "SOFTWARE_TOKEN_MFA",
"UserMFASettingList": [
"SMS_MFA",
"SOFTWARE_TOKEN_MFA"
]
}
MFA 质询是用户尝试进行身份验证时来自 AWS Cognito 的 API 响应,它是SMS_MFA
或SOFTWARE_TOKEN_MFA
。就我而言,我得到了一个SOFTWARE_TOKEN_MFA
挑战,但随后所有未来的挑战都会违背我的意愿恢复为SMS_MFA
.
如果我重复 TOTP 设置过程(从验证器应用程序中删除条目,重新验证等),我可以再次重复所有步骤。我的意思是,MFA 会期待一次 TOTP,然后在第一次之后再次恢复到 SMS。
任何人都可以阐明这种情况吗?你经历过吗?它是 AWS Cognito 中的已知问题/错误吗?难道我做错了什么?我觉得如果这个坏了,会产生相当大的噪音,但我找不到其他人有同样的问题。
我尝试过的事情:
- 在这里、谷歌和 AWS 论坛广泛搜索有相同问题的其他人。
- 我已经在 AWS 论坛上发帖,但没有任何结果: https ://forums.aws.amazon.com/thread.jspa?threadID=324131
- 在.
SelectMFAType
_aws-amplify-react
我将把这个自定义组件粘贴到这个问题的底部作为参考。 - 完全销毁并重新创建了 AWS Cognito 用户池。我认为它可能已损坏或无法正常工作。这没什么区别。
SetupTOTP.js
零件:
import React, { useContext, useEffect, useState } from 'react';
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContextText,
DialogTitle,
TextField,
} from '@material-ui/core';
import { Auth } from 'aws-amplify';
import { ToastsStore } from 'react-toasts';
import QRCode from 'qrcode.react';
import { AuthStateContext } from 'Context/auth-context';
const SetupTOTP = React.memo(props => {
const { open, handleClose } = props;
const { username } = useContext(AuthStateContext);
const [user, setUser] = useState(null);
const [qrCode, setQrCode] = useState('');
const [token, setToken] = useState('');
const handleSave = () => {
Auth.verifyTotpToken(user, token)
.then(() => {
Auth.setPreferredMFA(user, 'TOTP').then(() => {
ToastsStore.success('Token Verified Updated');
handleClose();
});
})
.catch(err => {
console.log(err);
ToastsStore.error(err.message);
});
};
useEffect(() => {
Auth.currentAuthenticatedUser().then(user => {
setUser(user);
});
}, []);
useEffect(() => {
if (username && user) {
Auth.setupTOTP(user).then(code => {
setQrCode(
`otpauth://totp/AWSCognito:${username}?secret=${code}&issuer=REDACTED`
);
});
}
}, [username, user]);
return (
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">Change Password</DialogTitle>
<DialogContent>
<QRCode value={qrCode} />
<TextField
margin="dense"
id="name"
label="Verify Token"
type="text"
fullWidth
onChange={e => setToken(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
</Button>
<Button onClick={handleSave} color="primary">
Save
</Button>
</DialogActions>
</Dialog>
);
});
export default SetupTOTP;