已生成 UserID 子声明,但从 Identity Server 4 令牌中消失,导致 UserManager.GetUser 失败

UserID sub-claim generated, but disappears from Identity Server 4 tokens, causing UserManager.GetUser to fail

我正在使用 Identity Server 4 和 dotnetcore 3.1 构建 React SPA。 Identity Server 客户端在 appsettings.json 中使用 IdentityServerSPA 配置文件定义。在前端使用 oidc-client ,我可以成功登录。检查 oidc 对象,我可以确认:

如果我随后尝试调用 API(使用 Authorization: Bearer 作为 id_tokenaccess_token),则没有 sub 声明当前的。因此,_userManager.GetUserAsync(User); 失败。

枚举并打印声明,我看到:

Claim: "System.Security.Claims.ClaimsIdentity" "nbf" "1601597732"
Claim: "System.Security.Claims.ClaimsIdentity" "exp" "1601598032"
Claim: "System.Security.Claims.ClaimsIdentity" "iss" "http://dev.openwork.shop:5000"
Claim: "System.Security.Claims.ClaimsIdentity" "aud" "OpenWorkShopAPI"
Claim: "System.Security.Claims.ClaimsIdentity" "iat" "1601597732"
Claim: "System.Security.Claims.ClaimsIdentity" "at_hash" "MoAqNfND0ct1mUFKpUtgcg"
Claim: "System.Security.Claims.ClaimsIdentity" "s_hash" "D81HZF_ii2r0i5-4_ZxnLA"
Claim: "System.Security.Claims.ClaimsIdentity" "sid" "cdMH0nFKdikldL1Gy3S3Eg"
Claim: "System.Security.Claims.ClaimsIdentity" "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" "fdf0023d-7ae9-4aaf-87fe-0f320b869171"
Claim: "System.Security.Claims.ClaimsIdentity" "auth_time" "1601582136"
Claim: "System.Security.Claims.ClaimsIdentity" "http://schemas.microsoft.com/identity/claims/identityprovider" "local"
Claim: "System.Security.Claims.ClaimsIdentity" "http://schemas.microsoft.com/claims/authnmethodsreferences" "pwd"

基于此,我可以通过访问名称声明直接查找 ID 来解决此问题:

string CurrentUserId => User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;

但这似乎既不正确也不理想。

我注意到我所有的 MySQL 表(AspNetUsers 除外)都是空的。例如,AspNetUserClaims 中没有值。然而,正如我上面所展示的,oidc-client 能够成功地看到用户 ID 的 sub 声明。

由于我使用的是 DefaultIdentityAddApiAuthorization,我不希望需要添加任何特殊逻辑来实现此类子声明,并且不确定为什么它在API 调用。

配置:

services.AddDefaultIdentity<UserProfile>(options => {
        options.SignIn.RequireConfirmedAccount = true;
        options.SignIn.RequireConfirmedEmail = true;
        options.Stores.MaxLengthForKeys = 64;
      }).AddEntityFrameworkStores<OWSData>();

      WebHostOptions webHost = configuration.GetSection("WebHost").Get<WebHostOptions>();
      string root = webHost.Url;

      services.AddIdentityServer((options) => {
        options.PublicOrigin = root;
        options.IssuerUri = root;
        options.UserInteraction.LoginUrl = $"{root}/account/login";
        options.UserInteraction.LogoutUrl = $"{root}/account/logout";
        options.UserInteraction.ErrorUrl = $"{root}/account/error";
        options.UserInteraction.ConsentUrl = $"{root}/account/terms-of-service";
        options.UserInteraction.DeviceVerificationUrl = $"{root}/account/device-verification";
      }).AddApiAuthorization<UserProfile, OWSData>(options => {
        // Serilog.Log.Information("Clients: {@clients}", options.Clients);
      });
      services.AddScoped<ICurrentUser, CurrentUser>();
      services.AddTransient<IReturnUrlParser, AuthReturnUrl>();

      // Auth (JWT)
      services.AddAuthentication(options => {
                 // options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
               })
              .AddCookie((options) => {
                 webHost.ConfigureCookie(CookieName, options.Cookie);
                 options.LoginPath = "/account/login";
                 options.AccessDeniedPath = "/account/denied";
                 options.LogoutPath = "/account/logout";
                 options.SlidingExpiration = true;
               })
              .AddIdentityServerJwt()
              .AddGoogle(options => {
                 ConfigureOAuth(options, "Google", configuration, webHost);
               })
              .AddGitHub(options => {
                 ConfigureOAuth(options, "GitHub", configuration, webHost);
                 options.Scope.Add("user:email");
                 options.EnterpriseDomain = configuration["GitHub:EnterpriseDomain"];
               });

      services.AddAuthorization(options => {
        // options.DefaultPolicy = new AuthorizationPolicy();
      });

      // Auth (Password)
      services.Configure<IdentityOptions>(options => {
        //Password settings
        options.Password.RequireDigit = true;
        options.Password.RequireLowercase = true;
        options.Password.RequireNonAlphanumeric = true;
        options.Password.RequireUppercase = true;
        options.Password.RequiredLength = 6;
        options.Password.RequiredUniqueChars = 1;

        //Lockout settings
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
        options.Lockout.MaxFailedAccessAttempts = 5;
        options.Lockout.AllowedForNewUsers = true;

        //User settings
        options.User.AllowedUserNameCharacters =
          "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@";
        options.User.RequireUniqueEmail = false;
      });

      // Cookies
      services.ConfigureApplicationCookie(options => {
        webHost.ConfigureCookie(CookieName, options.Cookie);
      });

JwtBearerAuthentication 中间件上的 JwtSecurityTokenHandler 默认映射一些声明。沿着这些映射,sub 声明被映射到 ClaimTypes.NameIdentifier。列出了映射 here.

您可以更改 API 上的代码以将名称标识符声明设置为 NameClaimTypeNameClaimType用于设置Identity.Name.

这是 API 上所需的代码更改:

services.AddAuthentication("Bearer").AddJwtBearer("Bearer",
                options =>
                {
                    options.Authority = "http://localhost:5000";
                    options.Audience = "api1";
                    options.RequireHttpsMetadata = false;

                    
                    options.TokenValidationParameters = new TokenValidationParameters()
                    {
                        NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"//To set Identity.Name 
                    };         
                });

这是一个示例工作代码:https://github.com/nahidf/IdentityServer4-adventures/blob/ids4-4/src/CoreApi/Startup.cs#L36

编辑:如果您使用 IdentityServerSPA 模板创建项目,它只是一个具有一些扩展方法的模板,并且在内部使用我们在手动设置中使用的相同方法

For-example per Docs IdentityServerJwt 是这样做的:

Represents an API that is hosted alongside with IdentityServer. The app is configured to have a single scope that defaults to the app name.

为了实现上述目的 IdentityServerJwt 正在内部调用 AddJwtBearer

并且它还使用 IdentityServerJwtDescriptor 添加 api 资源。

因此,如果我们进行手动设置或仅使用 IDS4 模板,我们可以在我们的代码中看到调用。

在这种情况下,因为您可以访问扩展方法内部调用的 AddJwtBearer,您可以在之后像这样更改 JwtBearerOptions

services.Configure<JwtBearerOptions>("Bearer",
                options =>
                {
                    new TokenValidationParameters()
                    {
                        NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"//To set Identity.Name 
                    };
                });

您还有另一个选项,它将删除我上面提到的所有映射,将此代码添加到 StartUp 的第一行。

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();