.net 8 C# 集成 AWS Cognito SMS/Email 注册与登录
本文主要分为三个部分:
1、描述 cognito 涉及的专业术语 以及 交互流程
2、.net 集成的代码
3、感想
* 阅读提示 :鼠标悬停在 章节标题 上可见 文章目录
1. Cognito 概念
1.1 关键词
进入 Amazon Cognito,会先看到 user pool 的列表
cognito
|
亚马逊(Amazon)云 提供的一种用户管理服务,简化用户注册、登录和授权、鉴权相关的服务操作。
可以在 cognito 上创建用户池,管理用户注册、登录和鉴权相关的问题;
可以创建 identity pool 对用户进行授权相关的约束;
支持多种身份验证方式,例如 用户 / 密码 登录,社交帐号( Facebook,Google 等)登录,企业身份供应商
|
user pool
|
用户池,多租户 / 不同的服务供应商,拥有自己的用户群体;
用户池支持用户注册、登录、鉴权、账号恢复等。
|
identity pool
|
资格授权池,可以提供一些 AWS 的资格认证到通过验证的用户上
|
app client
|
用户池所关联的应用服务端,必须要配置到相关的用户池上才允许访问该用户池
|
选定一个用户池,可以查看该用户池的信息,左侧栏见其 功能 / 配置
user pool 功能 / 配置 目录列表
* 为了降低描述的复杂度,此处仅做最小配置展开
* 加粗为需要重点关注;此处不展开 IdentityPool 以及 userPool.Security 和 userPool.Branding
|
Seconcd |
Third |
Description |
Remark |
User Pool
|
Overview |
|
用户池的基本信息 |
集成时需要找到这个用户池的 ID、ARN 等信息 |
Applications |
app client |
配置可访问该用户池的应用端信息 |
在此处可以限制一些读写的权限 |
|
User Management |
Users |
用户池里面的每个用户 |
相当于用户表,可以自定义用户属性 custom user attribute |
|
Groups |
用户分组,主要用于指定某个组内的用户可以享有某些权限 |
按需求来,可选配置 |
||
Authentication |
Authentication methods |
用户验证方式的配置 |
可以在此处配置密码验证 或 通过短信验证 或 通过邮箱验证 |
|
Sign-in |
用户登录时的相关配置 |
例如密码复杂度,登录方式 |
||
Sign-up |
用户注册时的相关配置 |
|
||
Social and external providers |
通过第三方登录的配置,像是通过 Facebook, Google, Amazon or Apple..等其他供应商来授权 |
|
||
Extensions |
通过一些自定义的验证行为,触发 AWS lambda function。此处是触发器的配置。 |
此处的触发条件是 cognito 规定好的。也即仅可以配置:要不要触发,触发哪一个function |
||
Security |
AWS WAF |
|
|
|
Threat protection |
|
|
||
Log streaming |
|
|
||
Branding |
Domain |
|
|
|
Managed login |
|
|
||
Message templates |
|
|
||
Identity Pool
|
Overview |
|
|
|
Authentication Providers |
|
|
|
|
Roles |
|
|
|
|
IAM Policies |
|
|
|
|
Data Synchronization |
|
|
|
Cognito 主要是做 用户管理 的事情,它支持通过 密码、手机短信、邮箱地址、其他企业供应商(例如 facebook、apple account 等) 等方式授权 token,
本文主要描述 自定义校验 custom authentication 的方式,此处要求在 user pool 中配置 3 个 AWS lambda function ,cognito 将会触发它的执行。
Lambda function 也是 Amazon web service 其中之一,此处可以简单地把其当作一个AWS的 api 方法。
这里涉及到 3 个触发器:define auth challenge
,create auth challenge
,verify auth challenge
Custom authentication trigger type
|
Define Auth Challenge |
custom authentication 的第一步,这里会返回 custom challenge name 到 cognito;同时也可以基于用户逻辑,直接在此处发布 token 。 |
Create Auth Challenge |
custom authentication 流程的第二步,实现自定义身份验证的步骤。通常在这一步中写入验证的答案,验证的问题可以是 captchas 或者其他安全问题 或 验证码 等。 |
|
Verify Auth Challenge |
custom authentication 的第三步,校验用户输入的答案与在 create auth challenge 中预设的答案是否一致 |
下文将描述我们的代码会如何与 cognito 交互,又是如何触发对应的 lambda function。
1.2. 流程图
登录步骤 1:输入账号
- 前端第一次调用接口,是提交登录的请求;
- 后端调用 AWSSDK.CognitoIdentifyProvider 中 cognitoClient.InitiateAuthAsync 方法,这个方法是 cognito 支持的自定义验证方式,它会自动触发到
define auth challenge
,create auth challenge
; - 此处发送短信的方式是后端去调用了一个 AWS Lambda Function
登录步骤 2:输入验证码
- 前端第二次调用的接口,是提交登录的验证码
- 后端会把验证码发送给 user pool 去做校验,通过了校验才能成功授权返回 token
* AWS Lambda Triggers
场景 |
SDK Function |
Trigger the Lambda |
sign-in 登录 |
InitiateAuthAsync |
Define Auth Challenge, Create Auth Challenge |
RespondToAuthChallengeAsync |
Verify Auth Challenge |
|
sign-up 注册 |
SignUpAsync |
Custom message trigger |
ConfirmSignUpAsync |
|
Define auth challenge
- 这个 trigger 用于决定下一个验证步骤,可以在这里配置走向密码验证或者自定义的验证方式。
- 基于正确的 user session 上,它进一步定义验证的流程
例如,用户通过密码验证之后,将进入到手机短信验证
// cognito 将会触发到配置在 user pool 上的 lambda trigger,event 是它的入参
exports.handler = async (event) => {
if (event.request.session.length === 1 && event.request.session[0].challengeName === 'SRP_A') {
// 是否直接发布 token
event.response.issueTokens = false;
// 是否验证失败
event.response.failAuthentication = false;
// 下一个验证方式的名字
event.response.challengeName = 'PASSWORD_VERIFIER';
} else if (event.request.session.length === 2 && event.request.session[1].challengeName === 'PASSWORD_VERIFIER' && event.request.session[1].challengeResult === true) {
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';
} else if (event.request.session.length === 3 && event.request.session[2].challengeName === 'CUSTOM_CHALLENGE' && event.request.session[2].challengeResult === true) {
event.response.issueTokens = true;
event.response.failAuthentication = false;
} else {
event.response.issueTokens = false;
event.response.failAuthentication = true;
}
return event;
};
Create auth challenge
- 一旦
Define Auth Challenge
触发器指明使用自定义验证,那么Create Auth Challenge
就会被触发去生成验证的内容。 - 在这个步骤中所创建的校验内容,是用户必须要答复的。例如 发到 SMS、Email 上的验证码,或者 CAPTCHA 之类的验证问题。
例如在 Create Auth Challenge
上设定一个将发送到 Email 的验证码(one-time password,简称 OTP),
此时 Amazon Cognito 其实会存储这个 event 内容,关联到用户的 session 和 用于 Verify Auth Challenge
步骤作为校验的答案(这里是验证码)
lambda function 实现示例:
exports.handler = async (event) => {
if (event.request.challengeName === 'CUSTOM_CHALLENGE') {
const otp = '654321'; // Generate or set your OTP here
event.response.publicChallengeParameters = { otp: 'Enter the OTP sent to your email' };
event.response.privateChallengeParameters = { otp: otp };
event.response.challengeMetadata = 'CUSTOM_CHALLENGE';
}
return event;
};
Verify auth challenge
- 该触发器在用户提交验证码之后被触发
- 该触发器接收到用户输入的 验证码 后会与
Create Auth Challenge
中的验证码相互匹配。如果校验正确,cognito 会处理返回的 event 并生成 token
lambda function 实现示例:
// TriggerSource is VerifyAuthChallengeResponse_Authentication
exports.handler = async (event) => {
const expectedOtp = event.request.privateChallengeParameters.otp;
const userResponse = event.request.challengeAnswer;
event.response.answerCorrect = userResponse === expectedOtp;
return event;
};
2. 集成到 .net 8 api
.NET 中需要使用 AWS SDK 提供的 API 来与 Cognito 进行交互,
引用包:AWSSDK.CognitoIdentityProvider
,下文代码使用的版本是 3.7.1.85
<PackageReference Include="AWSSDK.CognitoIdentityProvider" Version="3.7.1.85" />
添加引用
using Amazon.CognitoIdentityProvider;
using Amazon.CognitoIdentityProvider.Model;
cognitoClient 初始化:
var AWSregion = "ap-xxxxx-x";
_cognitoClient = new AmazonCognitoIdentityProviderClient(Amazon.RegionEndpoint.GetBySystemName(AWSregion));
2.1.1 注册 [signup]
public async Task<SignUpResponse> SignupAsync(SignUpDto user)
{
var userName = string.IsNullOrEmpty(user.Email) ? user.PhoneNumber : user.Email;
var request = new SignUpRequest
{
// 在 cognito user pool 中配置允许访问该 user pool 的 client 后,可以在cognito 上查看到这个 clientId
ClientId = _clientId,
// cognito 会校验代码复杂度,需要在 user pool 中配置;但此示例中密码是没有作用的
Password = "carcar@2024",
// 此处可以填手机号或邮箱地址; cognito user pool 中显示的 userName 是 user pool 用户管理意义上的 uuid
Username = userName
};
// 自定义的 user attribute
var nameAttribute = new AttributeType
{
Name = "name",
Value = user.Name
};
request.UserAttributes.Add(nameAttribute);
// 自定义的 user attribute
var emailAttribute = new AttributeType
{
Name = "email",
Value = user.Email
};
request.UserAttributes.Add(emailAttribute);
return await _cognitoClient.SignUpAsync(request);
}
可配置密码复杂度配置
可以配置对 user attribute 的必填要求
查看 user attribute
2.1.2 注册验证 [signup-confirm]
var confirmSignUpRequest = new ConfirmSignUpRequest
{
ClientId = "client id",
Username = "user phone / email",
ConfirmationCode = "verification code from sms or email"
};
var confirmSignUpResponse = await cognitoClient.ConfirmSignUpAsync(confirmSignUpRequest);
Console.WriteLine($"User {confirmSignUpResponse.UserConfirmed} confirmed successfully.")
2.1.3 重新发送验证码 [resend-confirmation]
// userName 可以是 cognito UserName,又或者是注册时写入的 userName 即邮箱或手机号
public async Task ResendConfirmationAsync(string userName)
{
var request = new ResendConfirmationCodeRequest
{
ClientId = _clientId,
Username = userName
};
var response = await _cognitoClient.ResendConfirmationCodeAsync(request);
}
2.2.1 登录 [signin]
var authRequest = new InitiateAuthRequest
{
ClientId = "the client id which configures in the user pool",
AuthFlow = AuthFlowType.CUSTOM_AUTH,
AuthParameters = new Dictionary<string, string>
{
{ "USERNAME", "your_username_or_phone_or_email" }
}
};
var authResponse = await cognitoClient.InitiateAuthAsync(authRequest);
2.2.2 登录验证 [signin-confirm]
校验验证码时,调用 RespondToAuthChallengeAsync
var respondToAuthChallengeRequest = new RespondToAuthChallengeRequest
{
ChallengeName = authResponse.ChallengeName,
ClientId = "your_client_id",
ChallengeResponses = new Dictionary<string, string>
{
{ "USERNAME", "your_username_or_phone_or_email" },
{ "SMS_MFA_CODE", "user_received_code" }
},
Session = authResponse.Session
};
var challengeResponse = await cognitoClient.RespondToAuthChallengeAsync(respondToAuthChallengeRequest);
var idToken = challengeResponse.AuthenticationResult.IdToken;
2.3 token 验证
在 Program 或 StartUp 的配置文件中,需要配置 Authentication 中间件
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = $"https://cognito-idp.{AWSregion}.amazonaws.com/{userPoolId}",
// ValidAudience = {userPoolId},
IssuerSigningKeyResolver = (s, securityToken, identifier, parameters) =>
{
// Get JsonWebKeySet from AWS
var json = new WebClient().DownloadString($"https://cognito-idp.{AWSregion}.amazonaws.com/{userPoolId}/.well-known/jwks.json");
// Deserialize the result
var keys = JsonConvert.DeserializeObject<JsonWebKeySet>(json).Keys;
// Cast the result to be the type expected by IssuerSigningKeyResolver
return (IEnumerable<SecurityKey>)keys;
}
};
});
3. 为什么写这一篇文章
很多开发可能和我一样,没有成为一名云上工程师,或者说项目里并没有使用 Amazon Web Service,那么对于如何集成 AWS 可能是不感兴趣的。
那我为什么写这篇文章?
1、国内对 AWS 的应用较少,AWS 相关的资料大多是英文的,其实解读下来真的挺花时间。
2、现在新颖的技术层出不穷,我的希望是在探索 cognito 的过程中,建立一个快速理解的方法论。
这背后考验的是专业知识以及逻辑梳理能力。我们一定是:知其所以然,才能应对多变的表象。
当然,这里我指的是对云服务如何集成传统应用,或者说“我的应用要怎么上云”。
3、借此机会看一下云服务的设计,现在都怎么玩的。
理解云服务的应用不单止为我们多提供一种解决方案,在排查集成云集成中产生的问题,也会有所启发。
是否能够以小见大,找到表象的本质?
在既定的解决方案架构里面找到可以拓展的共性,能不能经验迁移?
无论如何,希望这篇文章对你有所收益。
References
[1] Amazon Cognito Identity Provider examples using AWS SDK for .NET
[2] Authentication flow examples with .NET for Amazon Cognito
[3] Authenticating users in ASP.NET Core MVC using Amazon Cognito
[4] Securing ASP.NET Core API with JWT Token using AWS Cognito