在 ASP.NET Core 中映射、自定义和转换声明

作者:Damien Bowden

你可以根据任何用户或 identity 数据创建声明,并使用受信任的 identity 提供者或 ASP.NET Core identity 发出声明。 声明是一个名称值对,表示使用者是什么,而不是使用者可以做什么。 本文涵盖以下几个方面:

  • 如何使用 OpenID Connect 客户端配置和映射声明
  • 设置名称和角色声明
  • 重置声明命名空间
  • 使用 TransformAsync 自定义、扩展声明

使用 OpenID Connect 身份验证映射声明

配置文件声明可以在 id_token 中返回,后者在身份验证成功后返回。 ASP.NET Core 客户端应用只需要配置文件范围。 使用 id_token 返回声明时,不需要进行额外的声明映射。

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
       options.SignInScheme = "Cookies";
       options.Authority = "-your-identity-provider-";
       options.RequireHttpsMetadata = true;
       options.ClientId = "-your-clientid-";
       options.ClientSecret = "-your-client-secret-from-user-secrets-or-keyvault";
       options.ResponseType = "code";
       options.UsePkce = true;
       options.Scope.Add("profile");
       options.SaveTokens = true;
   });

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();

上述代码需要 Microsoft.AspNetCore.Authentication.OpenIdConnect NuGet 包。

获取用户声明的另一种方法是使用 OpenID Connect 用户信息 API。 ASP.NET Core 客户端应用使用 GetClaimsFromUserInfoEndpoint 属性配置此设置。 它与第一个设置的重要区别在于,必须使用 MapUniqueJsonKey 方法指定所需的声明,否则客户端应用中只有 namegiven_nameemail 标准声明可用。 id_token 中包含的声明按默认值进行映射。 这是与第一个选项的主要区别所在。 你必须显式定义所需的某些声明。

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
       options.SignInScheme = "Cookies";
       options.Authority = "-your-identity-provider-";
       options.RequireHttpsMetadata = true;
       options.ClientId = "-your-clientid-";
       options.ClientSecret = "-client-secret-from-user-secrets-or-keyvault";
       options.ResponseType = "code";
       options.UsePkce = true;
       options.Scope.Add("profile");
       options.SaveTokens = true;
       options.GetClaimsFromUserInfoEndpoint = true;
       options.ClaimActions.MapUniqueJsonKey("preferred_username",
                                             "preferred_username");
       options.ClaimActions.MapUniqueJsonKey("gender", "gender");
   });

var app = builder.Build();

// Code removed for brevity.

注意

如果标识提供者的发现文档显示支持 PAR,则默认的 Open ID Connect 处理程序就会使用推送授权请求 (PAR)。 identity 提供者的发现文档通常位于 .well-known/openid-configuration。 如果无法在 identity 提供者的客户端配置中使用 PAR,则可以使用 PushedAuthorizationBehavior 选项禁用 PAR。

builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect("oidc", oidcOptions =>
    {
        // Other provider-specific configuration goes here.

        // The default value is PushedAuthorizationBehavior.UseIfAvailable.

        // 'OpenIdConnectOptions' does not contain a definition for 'PushedAuthorizationBehavior'
        // and no accessible extension method 'PushedAuthorizationBehavior' accepting a first argument
        // of type 'OpenIdConnectOptions' could be found
        oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Disable;
    });

若要确保只有在使用 PAR 时身份验证才能成功,请改用 PushedAuthorizationBehavior.Require。 此更改还将新的 OnPushAuthorization 事件引入到 OpenIdConnectEvents,可用于自定义推送的授权请求或手动处理该请求。 有关更多详细信息,请参阅 API 建议

名称声明和角色声明映射

名称声明和角色声明映射到 ASP.NET Core HTTP 上下文中的默认属性。 有时需要对默认属性使用不同的声明,否则,名称声明和角色声明与默认值不匹配。 可以使用 TokenValidationParameters 属性映射声明,并根据需要将其设置为任何声明。 声明中的值可以直接在 HttpContext User.Identity.Name 属性和角色中使用。

如果 User.Identity.Name 没有值或缺少角色,请检查返回的声明中的值并设置 NameClaimTypeRoleClaimType 值。 可以在 HTTP 上下文中查看客户端身份验证返回的声明。

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
  .AddCookie()
  .AddOpenIdConnect(options =>
  {
       // Other options...
       options.TokenValidationParameters = new TokenValidationParameters
       {
          NameClaimType = "email"
          //, RoleClaimType = "role"
       };
  });

声明命名空间、默认命名空间

ASP.NET Core 添加了一些已知声明的默认命名空间,应用中可能不需要这些命名空间。 你可以选择禁用这些添加的命名空间并使用 OpenID Connect 服务器创建的确切声明。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

JsonWebTokenHandler.DefaultInboundClaimTypeMap.Clear();

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
       options.SignInScheme = "Cookies";
       options.Authority = "-your-identity-provider-";
       options.RequireHttpsMetadata = true;
       options.ClientId = "-your-clientid-";
       options.ClientSecret = "-your-client-secret-from-user-secrets-or-keyvault";
       options.ResponseType = "code";
       options.UsePkce = true;
       options.Scope.Add("profile");
       options.SaveTokens = true;
   });

var app = builder.Build();

// Code removed for brevity.
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
       options.SignInScheme = "Cookies";
       options.Authority = "-your-identity-provider-";
       options.RequireHttpsMetadata = true;
       options.ClientId = "-your-clientid-";
       options.ClientSecret = "-your-client-secret-from-user-secrets-or-keyvault";
       options.ResponseType = "code";
       options.UsePkce = true;
       options.Scope.Add("profile");
       options.SaveTokens = true;
   });

var app = builder.Build();

// Code removed for brevity.

如果需要按方案禁用命名空间而不是进行全局禁用,则可以使用“MapInboundClaims = false”选项。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
       options.SignInScheme = "Cookies";
       options.Authority = "-your-identity-provider-";
       options.RequireHttpsMetadata = true;
       options.ClientId = "-your-clientid-";
       options.ClientSecret = "-your-client-secret-from-user-secrets-or-keyvault";
       options.ResponseType = "code";
       options.UsePkce = true;
       options.MapInboundClaims = false;
       options.Scope.Add("profile");
       options.SaveTokens = true;
   });

var app = builder.Build();

// Code removed for brevity.

使用 IClaimsTransformation 扩展或添加自定义声明

IClaimsTransformation 接口可用于向 ClaimsPrincipal 类添加额外的声明。 该接口需要一个 TransformAsync 方法。 此方法可能会被多次调用。 仅当 ClaimsPrincipal 中没有时才添加新声明。 系统将创建 ClaimsIdentity 以添加新声明,并且可以将其添加到 ClaimsPrincipal 中。

using Microsoft.AspNetCore.Authentication;
using System.Security.Claims;

public class MyClaimsTransformation : IClaimsTransformation
{
    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        ClaimsIdentity claimsIdentity = new ClaimsIdentity();
        var claimType = "myNewClaim";
        if (!principal.HasClaim(claim => claim.Type == claimType))
        {
            claimsIdentity.AddClaim(new Claim(claimType, "myClaimValue"));
        }

        principal.AddIdentity(claimsIdentity);
        return Task.FromResult(principal);
    }
}

IClaimsTransformation 接口和 MyClaimsTransformation 类可以注册为服务:

builder.Services.AddTransient<IClaimsTransformation, MyClaimsTransformation>();

映射来自外部 identity 提供者的声明

请参阅以下文档:

在 ASP.NET Core 中保留来自外部提供程序的附加声明和令牌

你可以根据任何用户或 identity 数据创建声明,并使用受信任的 identity 提供者或 ASP.NET Core identity 发出声明。 声明是一个名称值对,表示使用者是什么,而不是使用者可以做什么。 本文涵盖以下几个方面:

  • 如何使用 OpenID Connect 客户端配置和映射声明
  • 设置名称和角色声明
  • 重置声明命名空间
  • 使用 TransformAsync 自定义、扩展声明

使用 OpenID Connect 身份验证映射声明

配置文件声明可以在 id_token 中返回,后者在身份验证成功后返回。 ASP.NET Core 客户端应用只需要配置文件范围。 使用 id_token 返回声明时,不需要进行额外的声明映射。

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
       options.SignInScheme = "Cookies";
       options.Authority = "-your-identity-provider-";
       options.RequireHttpsMetadata = true;
       options.ClientId = "-your-clientid-";
       options.ClientSecret = "-your-client-secret-from-user-secrets-or-keyvault";
       options.ResponseType = "code";
       options.UsePkce = true;
       options.Scope.Add("profile");
       options.SaveTokens = true;
   });

获取用户声明的另一种方法是使用 OpenID Connect 用户信息 API。 ASP.NET Core 客户端应用程序使用 GetClaimsFromUserInfoEndpoint 属性配置此设置。 它与第一个设置的重要区别在于,你必须使用 MapUniqueJsonKey 方法指定所需的声明,否则客户端应用程序中只有 namegiven_nameemail 标准声明可用。 id_token 中包含的声明按默认值进行映射。 这是与第一个选项的主要区别所在。 你必须显式定义所需的某些声明。

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
       options.SignInScheme = "Cookies";
       options.Authority = "-your-identity-provider-";
       options.RequireHttpsMetadata = true;
       options.ClientId = "-your-clientid-";
       options.ClientSecret = "-your-client-secret-from-user-secrets-or-keyvault";
       options.ResponseType = "code";
       options.UsePkce = true;
       options.Scope.Add("profile");
       options.SaveTokens = true;
       options.GetClaimsFromUserInfoEndpoint = true;
       options.ClaimActions.MapUniqueJsonKey("preferred_username", "preferred_username");
       options.ClaimActions.MapUniqueJsonKey("gender", "gender");
   }); 

名称声明和角色声明映射

名称声明和角色声明映射到 ASP.NET Core HTTP 上下文中的默认属性。 有时需要对默认属性使用不同的声明,否则,名称声明和角色声明与默认值不匹配。 可以使用 TokenValidationParameters 属性映射声明,并根据需要将其设置为任何声明。 声明中的值可以直接在 HttpContext User.Identity.Name 属性和角色中使用。

如果 User.Identity.Name 没有值或缺少角色,请检查返回的声明中的值并设置 NameClaimTypeRoleClaimType 值。 可以在 HTTP 上下文中查看客户端身份验证返回的声明。

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
       // other options...
       options.TokenValidationParameters = new TokenValidationParameters
       {
         NameClaimType = "email", 
         // RoleClaimType = "role"
       };
   });

声明命名空间、默认命名空间

ASP.NET Core 添加了一些已知声明的默认命名空间,应用中可能不需要这些命名空间。 你可以选择禁用这些添加的命名空间并使用 OpenID Connect 服务器创建的确切声明。

public void Configure(IApplicationBuilder app)
{
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

使用 IClaimsTransformation 扩展或添加自定义声明

IClaimsTransformation 接口可用于向 ClaimsPrincipal 类添加额外的声明。 该接口需要一个 TransformAsync 方法。 此方法可能会被多次调用。 仅当 ClaimsPrincipal 中没有时才添加新声明。 系统将创建 ClaimsIdentity 以添加新声明,并且可以将其添加到 ClaimsPrincipal 中。

public class MyClaimsTransformation : IClaimsTransformation
{
    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
       ClaimsIdentity claimsIdentity = new ClaimsIdentity();
       var claimType = "myNewClaim";
       if (!principal.HasClaim(claim => claim.Type == claimType))
       {		   
          claimsIdentity.AddClaim(new Claim(claimType, "myClaimValue"));
       }

       principal.AddIdentity(claimsIdentity);
       return Task.FromResult(principal);
    }
}

IClaimsTransformation 接口和 MyClaimsTransformation 类可以作为服务添加到 ConfigureServices 方法中。

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IClaimsTransformation, MyClaimsTransformation>();

在 ASP.NET Core 中扩展或添加自定义声明Identity

请参阅以下文档:

使用 IUserClaimsPrincipalFactory 添加声明到 Identity

映射来自外部 identity 提供者的声明

请参阅以下文档:

在 ASP.NET Core 中保留来自外部提供程序的附加声明和令牌