.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 challengecreate auth challengeverify 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 challengecreate 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 中间件

* token 参数说明 - 点此跳转查看

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

 

posted @ 2024-12-04 09:13  Carcar019  阅读(99)  评论(0编辑  收藏  举报