能否在Cognito的CUSTOM_AUTH流中触发MFA挑战?无需密码如何实现?
我们的应用使用Cognito进行用户认证与MFA处理,采用CUSTOM_AUTH流,通过defineauth、createauth和verifyauth Lambda实现了Cognito原生不支持的邮箱MFA。现需为部分操作实现额外MFA,计划根据用户配置触发对应MFA挑战,结合CUSTOM_CHALLENGE添加安全控制,但目前无法在CUSTOM_AUTH流中触发Cognito的MFA挑战。
测试场景与问题
简化测试用的createauth Lambda函数如下:
def lambda_handler(event, context): print("received event:", event) process_event(event) print("response event:", event) return event def process_event(event): if len(event["request"]["session"]) == 0: event["response"]["issueTokens"] = False event["response"]["failAuthentication"] = False event["response"]["challengeName"] = "SOFTWARE_TOKEN_MFA" return s = event["request"]["session"][len(event["request"]["session"]) - 1] if s["challengeName"] == "SOFTWARE_TOKEN_MFA" and s["challengeResult"]: event["response"]["issueTokens"] = True event["response"]["failAuthentication"] = False return event["response"]["issueTokens"] = False event["response"]["failAuthentication"] = True
该函数假设用户已启用SOFTWARE_TOKEN_MFA,将其作为首个挑战,验证通过后发放令牌。
执行初始化认证命令:
aws cognito-idp initiate-auth --auth-flow CUSTOM_AUTH \ --client-id <client-id> \ --region ap-southeast-2 \ --auth-parameters 'USERNAME=<username>'
返回结果符合预期:
{ "ChallengeName": "SOFTWARE_TOKEN_MFA", "Session": "<session>", "ChallengeParameters": { "USERNAME": "<user-id>" } }
但提交有效验证码(用户已正常配置MFA,可通过验证码登录)时:
aws cognito-idp respond-to-auth-challenge --client-id <client-id> --region ap-southeast-2 --challenge-name SOFTWARE_TOKEN_MFA --challenge-responses 'USERNAME=<user-id>,SOFTWARE_TOKEN_MFA_CODE=612345' --session <session>
出现错误:
An error occurred (CodeMismatchException) when calling the RespondToAuthChallenge operation: Invalid code or auth state for the user.
现咨询:能否在Cognito的CUSTOM_AUTH流中触发MFA挑战?若可行,如何无需用户密码实现?
1. 结论:可以实现,但不能直接返回SOFTWARE_TOKEN_MFA作为挑战类型
Cognito的CUSTOM_AUTH流中,所有挑战的验证逻辑完全由自定义Lambda接管,Cognito不会自动处理SOFTWARE_TOKEN_MFA这类原生挑战的校验。直接返回该挑战名会导致Cognito无法识别验证状态,从而抛出CodeMismatchException。
2. 无需用户密码的实现方案
核心思路是:先通过自定义挑战完成无密码身份确认(如邮箱/短信验证),再在Lambda中手动调用Cognito原生API验证MFA码,全程通过CUSTOM_AUTH流控制。
步骤1:调整Create Auth Challenge Lambda逻辑
该Lambda负责发起挑战,需分阶段处理无密码身份确认和MFA验证:
import boto3 cognito_idp = boto3.client('cognito-idp') def lambda_handler(event, context): process_event(event) return event def process_event(event): session = event["request"]["session"] username = event["request"]["userAttributes"]["sub"] user_pool_id = event["userPoolId"] # 第一阶段:首次会话,发起无密码身份确认挑战(示例为邮箱验证) if len(session) == 0: event["response"]["issueTokens"] = False event["response"]["failAuthentication"] = False event["response"]["challengeName"] = "CUSTOM_CHALLENGE" event["response"]["challengeParameters"] = { "CHALLENGE_TYPE": "EMAIL_VERIFICATION", "USERNAME": username } # 此处添加发送邮箱验证码的逻辑 send_verification_email(username) return last_challenge = session[-1] # 第二阶段:无密码验证通过,检查用户是否启用原生MFA if last_challenge["challengeName"] == "CUSTOM_CHALLENGE" and last_challenge["challengeResult"]: try: user_data = cognito_idp.admin_get_user( UserPoolId=user_pool_id, Username=username ) # 判断用户是否启用软件令牌MFA has_software_mfa = any( mfa["DeliveryMedium"] == "SOFTWARE_TOKEN" for mfa in user_data.get("UserMFASettingList", []) ) if has_software_mfa: # 发起MFA验证挑战 event["response"]["issueTokens"] = False event["response"]["failAuthentication"] = False event["response"]["challengeName"] = "CUSTOM_CHALLENGE" event["response"]["challengeParameters"] = { "CHALLENGE_TYPE": "SOFTWARE_TOKEN_MFA", "USERNAME": username } return else: # 用户未启用MFA,直接发放令牌 event["response"]["issueTokens"] = True event["response"]["failAuthentication"] = False return except Exception as e: print(f"获取用户信息失败:{str(e)}") event["response"]["failAuthentication"] = True return # 第三阶段:MFA验证结果处理 if (last_challenge["challengeName"] == "CUSTOM_CHALLENGE" and last_challenge["challengeParameters"]["CHALLENGE_TYPE"] == "SOFTWARE_TOKEN_MFA"): if last_challenge["challengeResult"]: event["response"]["issueTokens"] = True event["response"]["failAuthentication"] = False else: event["response"]["failAuthentication"] = True return # 默认情况:验证失败 event["response"]["issueTokens"] = False event["response"]["failAuthentication"] = True def send_verification_email(username): # 实现发送邮箱验证码的逻辑,比如调用SES或内部邮件服务 pass
步骤2:调整Verify Auth Challenge Lambda逻辑
该Lambda负责验证用户提交的挑战答案,需分别处理邮箱验证和MFA验证:
import boto3 cognito_idp = boto3.client('cognito-idp') def lambda_handler(event, context): challenge_type = event["request"]["challengeParameters"]["CHALLENGE_TYPE"] username = event["request"]["challengeParameters"]["USERNAME"] user_answer = event["request"]["challengeAnswer"] user_pool_id = event["userPoolId"] # 验证邮箱验证码 if challenge_type == "EMAIL_VERIFICATION": is_valid = verify_email_code(username, user_answer) event["response"]["answerCorrect"] = is_valid return event # 验证软件令牌MFA码 elif challenge_type == "SOFTWARE_TOKEN_MFA": try: cognito_idp.verify_software_token( UserPoolId=user_pool_id, Username=username, UserCode=user_answer, Session=event["request"]["session"] ) event["response"]["answerCorrect"] = True except cognito_idp.exceptions.CodeMismatchException: event["response"]["answerCorrect"] = False except Exception as e: print(f"MFA验证失败:{str(e)}") event["response"]["answerCorrect"] = False return event # 默认验证失败 event["response"]["answerCorrect"] = False return event def verify_email_code(username, code): # 实现邮箱验证码验证逻辑,比如从缓存或数据库查询对比 return code == "你的验证码" # 示例逻辑
步骤3:配置Lambda权限
确保Create Auth Challenge和Verify Auth Challenge Lambda的IAM角色拥有以下权限:
cognito-idp:AdminGetUsercognito-idp:VerifySoftwareToken- 若使用邮箱验证,需添加邮件服务权限(如
ses:SendEmail)
3. 关键注意事项
- 必须通过
CUSTOM_CHALLENGE封装原生MFA验证,不能直接返回SOFTWARE_TOKEN_MFA作为挑战名 - 无密码身份确认是前置步骤,确保用户身份合法后再触发MFA
- 调用
VerifySoftwareToken时,需传入正确的用户Session和用户池ID,否则会验证失败 - 确保用户已在Cognito控制台或通过API启用了SOFTWARE_TOKEN_MFA
内容的提问来源于stack exchange,提问作者Ivan Tomilov

