You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于C#的AWS Cognito服务器端CUSTOM_AUTH流实现问题

解决方案:服务器端实现AWS Cognito CUSTOM_AUTH邮箱MFA流程

核心调整方向

你的问题主要出在CUSTOM_AUTH流程的参数传递、Lambda触发器逻辑完整性,以及挑战响应的参数规范上。以下是分步修正方案:


1. 修正后端CUSTOM_AUTH初始化请求

将原认证方法改为CUSTOM_AUTH模式,同时传入用户名和密码供Lambda验证:

public async Task<AdminInitiateAuthResponse> InitiateCustomAuth(string username, string password)
{
    var authRequest = new AdminInitiateAuthRequest
    {
        UserPoolId = this._poolId,
        ClientId = this._clientId,
        AuthFlow = AuthFlowType.CUSTOM_AUTH,
        AuthParameters = new Dictionary<string, string>
        {
            {"USERNAME", username},
            {"PASSWORD", password} // 传递密码到Lambda做验证
        }
    };

    return await this._client.AdminInitiateAuthAsync(authRequest);
}

2. 完善三个Lambda触发器(缺一不可)

CUSTOM_AUTH需要三个Lambda触发器协作:DefineAuthChallenge(控制挑战流程)、CreateAuthChallenge(生成并发送OTP)、VerifyAuthChallengeResponse(验证OTP)。

2.1 DefineAuthChallenge Lambda(修正逻辑错误)

处理密码验证、挑战流转和失败次数限制:

import AWS from 'aws-sdk';
const cognito = new AWS.CognitoIdentityServiceProvider();

export const handler = async (event) => {
    const session = event.request.session || []; // 避免session为null

    // 第一次请求:验证用户名密码
    if (session.length === 0) {
        try {
            // 调用ADMIN_NO_SRP_AUTH验证密码
            await cognito.adminInitiateAuth({
                UserPoolId: event.userPoolId,
                ClientId: event.callerContext.clientId,
                AuthFlow: "ADMIN_NO_SRP_AUTH",
                AuthParameters: {
                    USERNAME: event.userName,
                    PASSWORD: event.request.challengeParameters.PASSWORD
                }
            }).promise();
            
            // 密码验证通过,触发OTP挑战
            event.response.issueTokens = false;
            event.response.failAuthentication = false;
            event.response.challengeName = "CUSTOM_CHALLENGE";
        } catch (err) {
            // 密码错误,拒绝认证
            event.response.issueTokens = false;
            event.response.failAuthentication = true;
        }
    }
    // 已发起OTP挑战,等待用户输入
    else if (session.length === 1 && session[0].challengeName === "CUSTOM_CHALLENGE" && !session[0].challengeResult) {
        event.response.issueTokens = false;
        event.response.failAuthentication = false;
        event.response.challengeName = "CUSTOM_CHALLENGE";
    }
    // OTP验证通过,颁发令牌
    else if (session.length > 0 && session.slice(-1)[0].challengeName === "CUSTOM_CHALLENGE" && session.slice(-1)[0].challengeResult) {
        event.response.issueTokens = true;
        event.response.failAuthentication = false;
    }
    // 超过3次OTP输入失败,锁定认证
    else if (session.length >= 3 && session.slice(-1)[0].challengeName === "CUSTOM_CHALLENGE" && !session.slice(-1)[0].challengeResult) {
        event.response.issueTokens = false;
        event.response.failAuthentication = true;
    }
    // 其他情况,继续OTP挑战
    else {
        event.response.issueTokens = false;
        event.response.failAuthentication = false;
        event.response.challengeName = "CUSTOM_CHALLENGE";
    }

    return event;
};

2.2 CreateAuthChallenge Lambda(生成并发送OTP)

生成6位验证码,通过SES发送到用户邮箱:

import AWS from 'aws-sdk';
const ses = new AWS.SES();

export const handler = async (event) => {
    if (event.request.challengeName === "CUSTOM_CHALLENGE") {
        // 生成6位随机OTP
        const otp = Math.floor(100000 + Math.random() * 900000).toString();
        
        // 存储OTP到私有参数,供后续验证使用
        event.response.privateChallengeParameters = { otp };
        
        // 发送邮件(替换为你的已验证发件邮箱)
        await ses.sendEmail({
            Source: "your-verified-email@example.com",
            Destination: { ToAddresses: [event.request.userAttributes.email] },
            Message: {
                Subject: { Data: "登录验证码" },
                Body: { Text: { Data: `你的登录验证码是:${otp}` } }
            }
        }).promise();
        
        // 给客户端返回挑战标识
        event.response.challengeMetadata = "EMAIL_OTP_CHALLENGE";
    }
    return event;
};

2.3 VerifyAuthChallengeResponse Lambda(验证OTP)

对比用户输入的验证码与存储的OTP:

export const handler = async (event) => {
    if (event.request.challengeName === "CUSTOM_CHALLENGE") {
        const expectedOtp = event.request.privateChallengeParameters.otp;
        const userInputOtp = event.request.challengeAnswer;
        
        // 验证验证码是否匹配
        event.response.answerCorrect = expectedOtp === userInputOtp;
    }
    return event;
};

3. 修正后端挑战响应方法

调整挑战响应的参数,使用Cognito规范的ANSWER键传递验证码,并包含用户名:

public async Task<AdminRespondToAuthChallengeResponse> RespondToCustomChallenge(string username, string otp, string session)
{
    return await this._client.AdminRespondToAuthChallengeAsync(new AdminRespondToAuthChallengeRequest() {
        ChallengeName = ChallengeNameType.CUSTOM_CHALLENGE,
        ClientId = this._clientId,
        UserPoolId = this._poolId,
        ChallengeResponses = new Dictionary<string, string> {
            { "USERNAME", username },
            { "ANSWER", otp } // Cognito默认用ANSWER作为验证码参数键
        },
        Session = session
    });
}

4. 端到端流程适配

  1. 前端提交用户名密码到后端,后端调用InitiateCustomAuth
  2. 如果返回ChallengeName=CUSTOM_CHALLENGE,后端将Session返回给前端,前端渲染验证码输入框
  3. 用户输入验证码后,前端提交验证码和Session到后端,后端调用RespondToCustomChallenge
  4. 验证通过后,后端用返回的AuthenticationResult生成自定义令牌,返回给前端

关键注意事项

  • 确保Lambda角色拥有cognito-idp:AdminInitiateAuth和ses:SendEmail的权限
  • 用户池App客户端需启用CUSTOM_AUTH认证流程
  • 确保用户邮箱已在Cognito中验证,否则无法发送OTP

内容的提问来源于stack exchange,提问作者Neophyte

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 02:25:49