已生成 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
对象,我可以确认:
- token_type:
Bearer
- 范围:
openid profile OpenWorkShopAPI
- profile.sub: (我的用户名)
- 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);
});
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
模板创建项目,它只是一个具有一些扩展方法的模板,并且在内部使用我们在手动设置中使用的相同方法
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();
我正在使用 Identity Server 4 和 dotnetcore 3.1 构建 React SPA。 Identity Server 客户端在 appsettings.json
中使用 IdentityServerSPA
配置文件定义。在前端使用 oidc-client
,我可以成功登录。检查 oidc
对象,我可以确认:
- token_type:
Bearer
- 范围:
openid profile OpenWorkShopAPI
- profile.sub: (我的用户名)
- 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);
});
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
模板创建项目,它只是一个具有一些扩展方法的模板,并且在内部使用我们在手动设置中使用的相同方法
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();