如何使用@aws-sdk/client-cognito-identity-provider的InitiateAuth命令触发SRP认证后进入CUSTOM_CHALLENGE流程?
我之前也碰到过类似的情况——一开始调用InitiateAuth直接跳自定义挑战,完全跳过了SRP的用户名密码验证。后来才搞明白问题出在两个地方:客户端调用的流程不对,还有用户池的触发器逻辑没配置到位。下面我一步步给你讲清楚怎么解决:
一、先确认用户池的基础配置
首先得确保你的Cognito用户池和应用客户端的认证流程是正确的:
- 打开用户池的“应用集成”页面,找到你的应用客户端,在“认证流程”里勾选
ALLOW_SRP_AUTH和ALLOW_CUSTOM_AUTH这两个选项,缺一不可。 - 必须配置好三个Lambda触发器:
Define Auth Challenge、Create Auth Challenge、Verify Auth Challenge Response,这三个是自定义挑战的核心,尤其是Define Auth Challenge的逻辑,要能正确判断什么时候进入自定义挑战。
二、客户端的正确调用流程(用@aws-sdk)
@aws-sdk本身没有封装SRP算法的实现,所以我们需要借助aws-cognito-identity-js来生成SRP所需的参数(自己写SRP算法太容易出错了)。下面是完整的调用步骤:
1. 生成SRP_A参数,发起SRP认证请求
首先用aws-cognito-identity-js的SRP类生成客户端公钥SRP_A,然后调用InitiateAuth,注意这里的AuthFlow必须是USER_SRP_AUTH,而不是CUSTOM_AUTH——这是很多人踩坑的点!
import { CognitoIdentityProviderClient, InitiateAuthCommand } from "@aws-sdk/client-cognito-identity-provider"; import { SRP } from "aws-cognito-identity-js"; const client = new CognitoIdentityProviderClient({ region: "你的区域" }); const userPoolId = "你的用户池ID"; const clientId = "你的应用客户端ID"; const username = "用户的用户名"; const password = "用户的密码"; // 生成SRP所需的参数 const srp = new SRP({ username: username, password: password, poolName: userPoolId.split("_")[1], // 取用户池ID的后半部分作为poolName clientId: clientId }); const srpA = srp.getSRPAModulus(); // 发起SRP认证请求 const initiateAuthCmd = new InitiateAuthCommand({ AuthFlow: "USER_SRP_AUTH", ClientId: clientId, AuthParameters: { USERNAME: username, SRP_A: srpA, // 如果你的应用客户端设置了密钥,需要计算SECRET_HASH:HMAC-SHA256(用户名+客户端ID, 客户端密钥) // SECRET_HASH: "计算后的SecretHash" } }); const initResponse = await client.send(initiateAuthCmd);
这一步如果用户名存在,Cognito会返回ChallengeName: "PASSWORD_VERIFIER",同时附带SRP验证所需的参数(比如USER_ID_FOR_SRP、SRP_B、SALT等)。
2. 处理PASSWORD_VERIFIER挑战,完成SRP验证
拿到上一步的响应后,我们需要计算SRP的验证签名,然后调用RespondToAuthChallenge来完成用户名密码的验证:
import { RespondToAuthChallengeCommand } from "@aws-sdk/client-cognito-identity-provider"; // 计算密码验证签名 const passwordVerifier = srp.getPasswordAuthenticationKey( initResponse.ChallengeParameters.USER_ID_FOR_SRP, initResponse.ChallengeParameters.SRP_B, initResponse.ChallengeParameters.SALT ); // 发起挑战响应请求 const respondToChallengeCmd = new RespondToAuthChallengeCommand({ ClientId: clientId, ChallengeName: "PASSWORD_VERIFIER", Session: initResponse.Session, ChallengeResponses: { USERNAME: username, PASSWORD_CLAIM_SECRET_BLOCK: initResponse.ChallengeParameters.SECRET_BLOCK, PASSWORD_CLAIM_SIGNATURE: passwordVerifier, TIMESTAMP: new Date().toISOString(), // 同样,有客户端密钥的话要带上SECRET_HASH // SECRET_HASH: "计算后的SecretHash" } }); const challengeResponse = await client.send(respondToChallengeCmd);
这一步完成后,Cognito会验证用户名密码是否正确。如果验证通过,就会触发我们配置的Define Auth Challenge Lambda,这时候就该让它引导流程进入自定义挑战了。
三、关键:配置Define Auth Challenge触发器的逻辑
这个Lambda的作用是决定当前要发起什么挑战,以及是否发放令牌。我们需要让它判断:当用户完成了SRP的PASSWORD_VERIFIER挑战后,就发起CUSTOM_CHALLENGE;当自定义挑战验证通过后,就发放令牌。
下面是一个示例代码:
exports.handler = async (event) => { // 情况1:用户刚完成SRP的密码验证,还没进行自定义挑战 if (event.request.session.length === 1 && event.request.session[0].challengeName === "PASSWORD_VERIFIER") { event.response.issueTokens = false; event.response.failAuthentication = false; event.response.challengeName = "CUSTOM_CHALLENGE"; } // 情况2:自定义挑战验证成功,发放令牌 else if (event.request.session.length === 2 && event.request.session[1].challengeName === "CUSTOM_CHALLENGE" && event.request.session[1].challengeResult === true) { event.response.issueTokens = true; event.response.failAuthentication = false; } // 情况3:自定义挑战失败,拒绝认证 else if (event.request.session.length > 1 && event.request.session[event.request.session.length - 1].challengeName === "CUSTOM_CHALLENGE" && event.request.session[event.request.session.length - 1].challengeResult === false) { event.response.issueTokens = false; event.response.failAuthentication = true; } // 默认情况:继续SRP流程(比如第一次发起请求时) else { event.response.issueTokens = false; event.response.failAuthentication = false; event.response.challengeName = "PASSWORD_VERIFIER"; } return event; };
至于Create Auth Challenge和Verify Auth Challenge Response的逻辑,就按照你自己的需求来写——比如在Create Auth Challenge里生成ABC-123456格式的OTP,然后发送给用户;在Verify Auth Challenge Response里验证用户输入的OTP是否正确。
四、最后处理自定义挑战
当RespondToAuthChallenge返回ChallengeName: "CUSTOM_CHALLENGE"时,就可以提示用户输入OTP,然后再次调用RespondToAuthChallenge:
const otp = "用户输入的ABC-123456格式的OTP"; const customChallengeCmd = new RespondToAuthChallengeCommand({ ClientId: clientId, ChallengeName: "CUSTOM_CHALLENGE", Session: challengeResponse.Session, ChallengeResponses: { USERNAME: username, ANSWER: otp, // 有客户端密钥的话带上SECRET_HASH // SECRET_HASH: "计算后的SecretHash" } }); const finalResponse = await client.send(customChallengeCmd); // finalResponse里就包含了IdToken、AccessToken等令牌
这样整个流程就走通了:先通过SRP验证用户名密码,再进入自定义OTP挑战,最后发放令牌。
备注:内容来源于stack exchange,提问作者blimkt

