【问题标题】:UserID sub-claim generated, but disappears from Identity Server 4 tokens, causing UserManager.GetUser to fail生成了 UserID 子声明,但从 Identity Server 4 令牌中消失,导致 UserManager.GetUser 失败
【发布时间】:2021-01-17 17:38:37
【问题描述】:

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

  • token_type:Bearer
  • 范围:openid profile OpenWorkShopAPI
  • profile.sub:(我的用户 ID)
  • profile.name:(我的用户名)

如果我随后尝试调用 API(使用 Authorization: Bearer 作为 id_token 或 access_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 声明。

由于我使用的是DefaultIdentity 和AddApiAuthorization,我预计不需要添加任何特殊逻辑来实现此类子声明,并且不确定为什么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);
      });

【问题讨论】:

    标签: .net-core asp.net-identity identityserver4 openid-connect


    【解决方案1】:

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

    您可以更改 API 上的代码以将名称标识符声明设置为 NameClaimType。 NameClaimType 用于设置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 模板创建项目,它只是一个具有一些扩展方法的模板,并且在内部使用我们在手动设置中使用的相同方法

    例如DocsIdentityServerJwt 就是这样做的:

    表示与 IdentityServer 一起托管的 API。 该应用程序配置为具有默认为应用程序名称的单个范围。

    为了达到上述目的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();
    

    【讨论】:

    • 我对@9​​87654341@ 和AddJwtBearer 之间的区别感到困惑。根据查看前者的代码,您的代码似乎省略了添加IdentityServerJwtDescriptor 之类的内容。这是故意的吗?它会破坏我的应用程序中的其他内容吗?如果没有这段代码,React-Auth 模板如何工作得很好?我直接从身份 UI 脚手架复制了用户管理器代码。我的意思是,身份服务器不应该开箱即用吗?用户管理器自带的,为什么我要添加特殊代码才能让它的默认用户登录生效?
    • 我试图在编辑回复时解释这些。关于为什么反应模板有效,我建议你检查JwtSecurityTokenHandler.DefaultInboundClaimTypeMap 看看发生了什么。这个问题与 IdentityServer 无关,它都发生在 JwtSecurityTokenHandler
    • 感谢您如此周到!
    猜你喜欢
    • 1970-01-01
    • 2017-05-14
    • 2022-10-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-01
    • 2017-10-06
    • 2020-05-01
    相关资源
    最近更新 更多