使用 Amazon Cognito 为 .NET 应用程序实施基于角色的访问控制

作者: 拉杰什·西塔拉曼 |

Ulili Nhaga 为这篇文章做出了贡献。

在构建应用程序时,确保适当的安全和访问控制至关重要。实现这一目标的一种方法是实现基于角色的访问控制 (RBAC) 授权系统。这篇博文将讨论使用带有 OpenID Connect (OIDC) 的 Amazon Cognito 为 .NET 应用程序实现 RBAC。我们将指导您如何构建可随您的应用程序扩展的强大访问控制系统。

使用 Amazon Cognito 实现安全的客户身份和访问管理。它提供了安全的身份存储和联盟选项,可扩展到数百万用户。它还允许用户使用社交媒体身份提供商、基于 SAML 或 OIDC 的身份提供商以及 OAuth 2.0、SAML 2.0 和 OpenID Connect 等标准登录。

场景

假设一个有两个用户 Bob 和 Sarah 的场景。Bob 在一个必须对 Amazon Simple Storage Service (S3) 存储桶中的项目具有只读权限的团队工作。Sarah 的团队必须有权在同一 S3 存储桶中读取和写入项目。为了在使用 Amazon Cognito 的 .NET 应用程序中针对这种场景实现 RBAC,我们将:

  • 使用 Amazon Identity and Access Management (IAM) 创建"只读角色"和"读写角色"。
  • 授予这些角色在 S3 存储桶上读取和读写项目的相应权限。
  • 创建两个 Cognito 用户组,"只读组"和"读写组",并将每个用户组映射到相应的 IAM 角色。
  • 设置 .NET 应用程序,将每个 Cognito 用户组映射到基于 .NET 策略的角色检查。
  • 最后,根据用户的权限,将用户 BobSarah 添加到 Cognito 用户组中。

BobSarah 进行身份验证时,.NET 应用程序会根据其角色强制执行身份验证和授权。为了允许用户在 S3 存储桶上读取或写入项目,前端使用 Cognito 的软件开发工具包将 ID 代币与临时 IAM 凭证进行交易。临时 IAM 凭证根据映射到其 Cognito 用户群组的 IAM 角色限制 Bob 或 Sarah 对 S3 存储桶的访问权限。

架构概述

在我们的场景中,.NET 应用程序的工作原理如图 1 所示。它有两个 .NET 组件:一个使用 Razor 页面构建的前端接口 (UI) 和一个使用 .NET 8 Web API 构建的后端。我们为身份验证和授权实施 OIDC 授权授权代码流。我们正在使用 Cognito 的内置用户界面进行用户登录。当像 BobSarah 这样的用户尝试访问受限页面以在 S3 存储桶上读取或写入项目时,将发生以下情况:

  • 前端将它们重定向到 Cognito 用户界面进行登录。
  • 用户输入他们的证书。
  • 验证用户凭据后,Cognito 生成授权码并将用户重定向回前端。
  • 前端使用授权码向 Cognito 请求三个 JSON 网络令牌 (JWT) 令牌(ID 令牌、访问令牌和刷新令牌)。
  • Cognito 在验证授权码后发出 JWT 令牌。

前端应用程序将三个 JWT 令牌加密存储在用户浏览器的 cookie 中。然后,它在向后端 API 发送的每个 HTTP 请求中使用 ID 令牌。后端使用 ID 令牌接收并验证 HTTP 请求。然后,它使用 Cognito 软件开发工具包在 Amazon IAM 中使用该身份令牌换取临时安全证书。这些证书将 IAM 角色与使用 S3 存储桶的权限相匹配。

图 1-身份验证流程

图 1 — 身份验证流程

先决条件

在此示例中,使用 GitHub 存储库 aws-samples/sample-rbac-dotnet-amazon-cognito,其中包含 .NET 应用程序的示例代码和用于部署 Cognito 的基础设施即代码 (IaC) 脚本。IaC 脚本使用带有 Typescript 的亚马逊云科技云开发套件 (CDK) 来定义和部署资源。

  • .NET 8.0 软件开发套件 (SDK)
  • Git CLI 入门
  • 亚马逊云科技命令行界面 (亚马逊云科技 CLI)
  • 亚马逊云科技 CDK v2
  • Amazon Cognito
  • Amazon Systems Manager 参数存储库
  • Amazon Secrets Manager
  • Git 下载
  • 在本地配置的亚马逊云科技账户的亚马逊云科技证书
  • Visual Studio Code(或者你最喜欢的 IDE)
  • Jq,一款轻巧灵活的命令行 JSON 处理器

代码漫步

使用以下命令从终端克隆存储库:

git clone https://github.com/aws-samples/sample-rbac-dotnet-amazon-cognito.git

在像 Visual Studio Code 这样的集成开发环境中打开解决方案来探索实现。

映射 RBAC 角色

要在 .NET 应用程序上实施 RBAC 以访问亚马逊云科技资源(如 S3 存储桶),首先创建 Amazon IAM 角色并按照最低权限访问原则向他们授予权限。这样可以确保用户以最低的权限对应用程序所需的特定资源执行所需的操作。以下 CDK 代码说明了具有"读写"权限的 IAM 角色,它允许经过身份验证的 Cognito 用户代入此角色:

const readWriteRole = new iam.Role(this, "s3-read-Write-role", {
  assumedBy: new iam.WebIdentityPrincipal(
    "cognito-identity.amazonaws.com",
    {
      StringEquals: {
        "cognito-identity.amazonaws.com:aud": `${props.IdentityPoolId}`,
      },
      "ForAnyValue:StringLike": {
        "cognito-identity.amazonaws.com:amr": "authenticated",
      },
    }
  ),
});

readWriteRole.addToPolicy(
  new iam.PolicyStatement({
    actions: [
      "s3:PutObject",
      "s3:GetObject",
      "s3:GetBucketLocation",
      "s3:ListBucket",
    ],
    resources: [
      props.SampleBucket.arnForObjects("*"),
      props.SampleBucket.bucketArn,
    ],
    effect: iam.Effect.ALLOW,
    sid: "AllowReadWrite",
  })
);
JSON

接下来,我们创建 Cognito 用户组,将其与 IAM 角色关联,然后创建用户并将其分配到群组。以下代码说明了"读写组"组和用户"Sarah"的创建。

const readWriteUser = new cognito.CfnUserPoolUser(this, "read-write-user", {
  userPoolId: props.UserPoolId,
  username: "sarah",
});

const readWriteGroup = new cognito.CfnUserPoolGroup(
  this,
  "read-write-group",
  {
    userPoolId: props.UserPoolId,
    description: "Illustrates read-write user groups",
    groupName: "read-write-group",
    precedence: 0,
    roleArn: props.IamReadWriteRoleArn,
  }
);

const readWriteAttach = new cognito.CfnUserPoolUserToGroupAttachment(
  this,
  "read-write-attach",
  {
    groupName: readWriteGroup.groupName as string,
    username: readWriteUser.username as string,
    userPoolId: props.UserPoolId,
  }
);
JSON

然后,设置 .NET 应用程序,将每个 Cognito 用户组与其角色映射。.NET 有多种定义角色的方法。对于我们的场景,我们将使用基于策略的角色检查来验证用户是否有特定的声明。在我们的例子中,Cognito 的用户组将使用申领密钥 cognito:groups。声明值是我们在 Cognito 中创建的群组的名称,即 read-only-groupread-write-group

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ReadOnlyRole", policy => policy.RequireClaim("cognito:groups", "read-only-group", "read-write-group"));
    options.AddPolicy("ReadWriteRole", policy => policy.RequireClaim("cognito:groups", "read-write-group"));
});
C#

最后,在映射所有组件中的 RBAC 角色后,在 .NET 应用程序上使用这些角色来强制具有特定角色的用户访问受限页面或端点。以下是 Razor Page 和 Web API 代码示例,这些示例限制了具有特定角色的授权用户的页面和终端节点。

//Razor Page
[Authorize(Policy = "ReadWriteRole")]
public class CreatePage : PageModel
{
    ...
    public void OnGet()
    {
    }
    ...
}

//Web API
app.MapGet("/GetData", [Authorize(Policy = "ReadOnlyRole")] async Task
           
           
           
             (...) => { ... return Results.Ok(result); }) .WithName("GetData") .WithOpenApi(); 
           
C#

将 Amazon Cognito 设置为在前端 Razor Page .NET 应用程序上使用 OpenID Connect (OIDC)

为确保用户在浏览时获得流畅的体验,.NET 应用程序必须保留用户的会话,以避免用户每次打开新页面、窗口或选项卡时都打开登录页面。通过将 OIDC 与加密 Cookie 相结合,您可以实现永久的用户会话。OIDC 将使用 Cognito 处理身份验证流程。身份验证流程完成并生成 JWT 令牌后,应用程序使用加密的 Cookie 将用户的会话保留在浏览器中。

以下代码举例说明了如何配置 .NET Razor 页面以将 OIDC 与加密 Cookie 结合起来。该代码正在设置 DefaultAuthenticateSchemeDefaultSignInScheme 作为 cookie,DefaultChallengeScheme 作为 OIDC。要设置 .NET OIDC 与 Cognito 的集成,我们使用 .NET NuGet 软件包 Microsoft.aspnetCore.Authentication.OpenidConnect。该代码从 Amazon Secrets Manager 获取"网页机密"密钥。我们在 IaC 部署期间创建了这个密钥来存储权限客户端 ID客户端密钥。OIDC 需要这些信息来连接 Cognito,我们用这些信息 MetadataAddress 来告诉 OIDC 在哪里动态发现 Cognito 的端点和元数据。

builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(options =>
    {
        SecretsManagerCache secretsManager = new();
        string clientSecret = secretsManager.GetSecretString("web-page-secrets").Result ?? "{}";
        var idConfig = JsonSerializer.Deserialize
           
           
           
             (clientSecret) ?? new(); options.MetadataAddress = idConfig.Authority + "/.well-known/openid-configuration"; options.ClientId = idConfig.ClientId; options.ClientSecret = idConfig.ClientSecret; options.Authority = idConfig.Authority; options.CallbackPath = "/signin-oidc"; options.ResponseType = OpenIdConnectResponseType.Code; options.SignedOutCallbackPath = "/signedout-oidc"; options.UseTokenLifetime = true; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, }; options.SaveTokens = true; options.Events = new OpenIdConnectEvents { OnRedirectToIdentityProviderForSignOut = OnRedirectToIdentityProviderForSignOut }; }); var app = builder.Build(); ... app.UseAuthentication(); app.UseAuthorization(); app.MapRazorPages(); ... await app.RunAsync(); 
           
C#

设置 Amazon Cognito 以便在后端 Web API .NET 应用程序上使用 JWT

Web API .NET 应用程序要求在 OIDC 身份验证协议的每个 HTTP 请求标头中都有一个 JWT 令牌。与 Razor Pages 不同,它不需要保留用户的会话。要在 Web API .NET 应用程序上实现身份验证,我们需要使用 JWT。以下代码示例显示了如何设置 JWT 以使其与 Cognito 一起使用。就像我们介绍的 OIDC 实现一样,该代码还读取了密钥管理器中的机密信息,并设置了与 Cognito 集成的参数。

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

}).AddJwtBearer(options =>
{
    SecretsManagerCache secretsManager = new();
    string clientSecret = secretsManager.GetSecretString("web-api-secrets").Result ?? "{}";
    var idConfig = JsonSerializer.Deserialize
           
           
           
             (clientSecret) ?? new(); options.Authority = idConfig.Authority; options.MetadataAddress = idConfig.Authority + "/.well-known/openid-configuration"; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = idConfig.Authority, ValidateLifetime = true, LifetimeValidator = (before, expires, token, param) => expires.HasValue && expires.Value > DateTime.UtcNow, ValidateAudience = false, ValidateIssuerSigningKey = true, }; }); ... var app = builder.Build(); ... app.UseAuthentication(); app.UseAuthorization(); await app.RunAsync(); 
           
C#

部署和演示

GitHub 存储库 aws-samples/sample-rbac-dotnet-amazon-cognito 提供了完整的演示。为 Cognito 和 Amazon IAM 角色部署 IAC 脚本,并在本地运行两个 .NET 应用程序以进行演示。

部署

执行以下命令来部署 CDK IaC 项目。

cd src/infra/cognito
npm install
cdk bootstrap --all
cdk synth --all
cdk deploy --require-approval never --all

通过执行以下命令来设置用户(BobSarah)密码。将占位符 REPLACE_THIS_PLACEHOLDER_PASSWORD 替换为每个用户的密码。

export USER_POOL_ID=$(aws cognito-idp list-user-pools --max-results 10 | jq ".UserPools[] | select(.Name == \"rbacauthz\") | .Id" -r)
aws cognito-idp admin-set-user-password --user-pool-id $USER_POOL_ID --username bob --pass REPLACE_THIS_PLACEHOLDER_PASSWORD --permanent
aws cognito-idp admin-set-user-password --user-pool-id $USER_POOL_ID --username sarah --pass REPLACE_THIS_PLACEHOLDER_PASSWORD --permanent

演示

完成部署后,从克隆的示例解决方案所在的文件夹中打开两个新终端。执行以下命令导航到 .NET 项目并在本地运行它们以测试解决方案。首先运行后端 Web API 应用程序,然后运行前端 Razor Page。

终端 1:运行后端 Web API .NET 应用程序

cd src/apps/SampleWebApi
dotnet dev-certs https
dotnet build
dotnet run

终端 2:运行前端 Razor Page .NET 应用程序

cd src/apps/WebPage
dotnet build
dotnet watch

在终端 2 执行命令后,浏览器将打开,请求您允许"本地主机"TLS .NET 开发证书。按照说明进行接受,然后您将通过 https://localhost:7016/ 网址访问演示应用程序。它的页面如图 2 所示。

图 2 — 主页

图 2 — 主页

图 2 还显示,从主页右上角选择"登录"会将您重定向到 Cognito 用户界面进行身份验证,如图 3 所示。

图 3 — Cognito 用户界面

图 3 — Cognito 用户界面

当您输入用户名 sarah 和您定义的密码时,应用程序会将您重定向到包含 Sarah 个人资料的前端页面,如图 4 所示。用户个人资料页面包含莎拉从 Cognito 代币中提取的声明。从左上角的菜单选项中选择"图书",模拟向 S3 存储桶添加物品。

图 4 — 用户资料页面

图 4 — 用户资料页面

图书页面将显示一个空列表。选择"添加新书"按钮。您将导航到"新书"页面,输入图书信息,如图 5 所示。填写表格并选择"提交"按钮。该应用程序会验证您的信息并向后端调用 HTTP 请求。该请求将拥有来自当前会话的 ID 令牌,用于将图书信息作为项目插入 S3 存储桶。多次重复这些过程以获得样本数据。

图 5 — 新书单

图 5 — 新书单

输入多个图书样本数据后,选择右上角的注销选项,如图 6 所示,从 Sarah 切换到 Bob。

图 6 — 注销选项

图 6 — 注销选项

再次选择"登录"并输入 Bob 的凭据以模拟具有只读角色的用户。输入 Bob 的凭据后,您将访问 Bob 的用户个人资料。Bob 声称显示了"只读组"Cognito 用户组,如图 7 所示。

图 7-Bob 的用户资料

图 7 — Bob 的用户资料

选择"图"菜单选项并尝试添加一本新书。您将收到一个访问被拒绝的页面,如图 8 所示,因为 Bob 属于只读组。他的角色阻止他们将项目写入 S3 存储桶。

图 8- 拒绝访问页面

图 8 — "拒绝访问"页面

清理

删除通过运行此演示创建的资源,以避免不必要的费用。为此,请按照克隆存储库的文件夹中的命令进行操作。

cd src/infra/cognito
cdk destroy --all

结论

在这篇文章中,我们展示了如何使用 Amazon Cognito 和 OpenID Connect 为 .NET 应用程序创建具有基于角色的访问控制的强大且可扩展的访问控制系统。我们创建了 IAM 角色,将其映射到 Cognito 用户组,并将它们与基于 .NET 策略的角色检查保持一致,以控制对亚马逊云科技资源(如 S3 存储桶)的访问权限。OIDC 授权授权代码流程、Cognito 的内置用户登录用户界面以及将 ID 令牌交换为临时 IAM 安全证书展示了安全而简单的身份验证和授权流程。您可以利用这些相同的方法来实现 RBAC 并增强您自己的 .NET 应用程序的安全性,同时确保用户的访问权限最低。有关 Amazon Cognito 的更多信息。请参阅 Amazon Cognito 开发者指南。



拉杰什·西塔拉曼

拉杰什·西塔拉曼

拉杰什·西塔拉曼是亚马逊云科技专业服务的云架构师。他帮助客户进行架构、开发和重新设计应用程序,以充分利用亚马逊云科技云。他专注于无服务器、微服务架构和生成式 AI。


*前述特定亚马逊云科技生成式人工智能相关的服务仅在亚马逊云科技海外区域可用,亚马逊云科技中国仅为帮助您发展海外业务和/或了解行业前沿技术选择推荐该服务。