IdentityServer4 - 子声明丢失
IdentityServer4 - sub claim is missing
我有一个 IdentityServer4 实例,我正试图在 nginx 代理后面的 Docker 容器中获取 运行ning。我基于 Git 存储库中的 AspNet 身份示例,但在用户成功注册新帐户后,我从 IdentityServer 获得 "An error occurred" 并且日志显示
[07:46:39 ERR] An unhandled exception has occurred: sub claim is missing
System.InvalidOperationException: sub claim is missing
at IdentityServer4.IdentityServerPrincipal.AssertRequiredClaims(ClaimsPrincipal principal
at IdentityServer4.Hosting.IdentityServerAuthenticationService.AugmentPrincipal(ClaimsPrincipal principal
at IdentityServer4.Hosting.IdentityServerAuthenticationService.<SignInAsync>d__7.MoveNext
我的Startup.cs看起来像这样
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().
var connectionString = Configuration.GetConnectionString("DefaultConnection");
var issuerUri = Configuration.GetSection("IssuerUri").Value;
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddTransient<IEmailSender, EmailSender>();
services.AddMvc();
services.AddCors(o => o.AddPolicy("CorsPolicy", b =>
{
b.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}));
services.AddIdentityServer(options =>
{
options.IssuerUri = issuerUri;
options.PublicOrigin = issuerUri;
})
.AddDeveloperSigningCredential()
// this adds the config data from DB (clients, resources)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
// this enables automatic token cleanup. this is optional.
//options.EnableTokenCleanup = true;
//options.TokenCleanupInterval = 30;
});
我一定是错过了一些明显的配置,但我看不到在哪里。有什么想法吗?
更新
我在这方面取得了一些进展,似乎已经克服了最初的错误。用户现已通过身份验证,但 signin-oidc 页面抛出错误
[11:33:21 INF] Request starting HTTP/1.1 POST http://mvcportal.co.uk/signin-oidc application/x-www-form-urlencoded 1565
[11:33:21 INF] AuthenticationScheme: Cookies signed in.
[11:33:21 INF] Request finished in 684.8425ms 302
[11:33:27 INF] Request starting HTTP/1.1 POST http://mvcportal.co.uk/signin-oidc application/x-www-form-urlencoded 1565
[11:33:27 ERR] Message contains error: 'invalid_grant', error_description: 'error_description is null', error_uri: 'error_uri is null', status code '400'.
我有一个有效的 JWT,但我注意到 idp 不等于发行者。对吗?
{
"nbf": 1508758474,
"exp": 1508758774,
"iss": "http://myproxiedlogonsitebehindnginx.co.uk",
"aud": "mvc.portal",
"nonce": "636443552746808541.MGVjMzk2NTEtYmYwNS00NmQwLTllOTQtZDVjNjdlYTA2YWVlYTQ3Zjg1NjgtZDA1Yi00NDE0LWJiYmYtMjM4YzI1NjZlYTcx",
"iat": 1508758474,
"c_hash": "kG7wG8vSgRe5zdriHQ6iMA",
"sid": "c9410ee8f27b69c32e43d5ac3d407f37",
"sub": "e80fb854-cab2-4381-8057-19de0fea73f4",
"auth_time": 1508757008,
"idp": "local",
"amr": [
"pwd"
]
}
更新 2
如果有帮助,这是 idsrv 上的客户端配置
new Client
{
ClientId = "mvc.portal",
ClientName = "Customer Portal",
ClientUri = customerPortalBaseUri,
ClientSecrets =
{
new Secret("21f51463-f436-4a84-92ce-1b520dd63a81".Sha256())
},
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
AllowAccessTokensViaBrowser = false,
RedirectUris = { $"{customerPortalBaseUri}/signin-oidc"},
FrontChannelLogoutUri = $"{customerPortalBaseUri}/signout-oidc",
PostLogoutRedirectUris = { $"{customerPortalBaseUri}/signout-callback-oidc" },
AllowOfflineAccess = true,
RequireConsent = false,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email
}
}
这是 client/portal 配置
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "oidc";
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "http://myproxiedlogonsitebehindnginx.co.uk";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc.portal";
options.ClientSecret = "21f51463-f436-4a84-92ce-1b520dd63a81";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
});
更新 3
所以现在我确信它与部署有关,因为如果我 运行 我本地机器上的 mvc 应用程序但使用容器中部署的 idsvr(在 nginx 后面)我可以毫无问题地进行身份验证,但是如果我尝试容器化门户的版本,我仍然会得到一个未处理的 500,但它没有被记录,然后如果我重试该操作,我会得到这个记录:
[11:22:51 INF] Request starting HTTP/1.1 POST http://mvcportal.co.uk/signin-oidc application/x-www-form-urlencoded 1559
[11:22:51 ERR] Message contains error: 'invalid_grant', error_description: 'error_description is null', error_uri: 'error_uri is null', status code '400'.
[11:22:51 ERR] Exception occurred while processing message.
Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectProtocolException: Message contains error: 'invalid_grant'], error_description: 'error_description is null', error_uri: 'error_uri is null'.
所以我终于弄清楚了。似乎 nginx 中的默认 header 限制不太好用,我在日志中发现了这一点
upstream sent too big header while reading response header from upstream
更新 nginx 配置以包含行
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
防止了 502 错误,现在所有验证都正常了。
感谢您的回复亲爱的丹,
另外,我得到了这个错误,我已经解决了这个问题
在 Startup.cs 配置服务中找到这一行
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores()
.AddIdentityServer()
.AddDefaultTokenProviders();
确保 .AddIdentityServer()
在那里。
我遇到了完全相同的问题并通过添加解决了它:
.AddAspNetIdentity<ApplicationUser>();
至
services.AddIdentityServer()
在Startup.cs
services.AddIdentityServer()
.AddAspNetIdentity<ApplicationUser>()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString,
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.MigrationsAssembly(migrationsAssembly);
});
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString,
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.MigrationsAssembly(migrationsAssembly);
});
}).Services.AddTransient<IProfileService, ProfileService>();
和
ProfileService.cs
public class ProfileService : IProfileService
{
private readonly UserManager<ApplicationUser> _userManager;
public ProfileService(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
async public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var subject = context.Subject ?? throw new ArgumentNullException(nameof(context.Subject));
var subjectId = subject.Claims.Where(x => x.Type == "sub").FirstOrDefault().Value;
var user = await _userManager.FindByIdAsync(subjectId);
if (user == null)
throw new ArgumentException("Invalid subject identifier");
var claims = GetClaimsFromUser(user);
context.IssuedClaims = claims.ToList();
}
async public Task IsActiveAsync(IsActiveContext context)
{
var subject = context.Subject ?? throw new ArgumentNullException(nameof(context.Subject));
var subjectId = subject.Claims.Where(x => x.Type == "sub").FirstOrDefault().Value;
var user = await _userManager.FindByIdAsync(subjectId);
context.IsActive = false;
if (user != null)
{
if (_userManager.SupportsUserSecurityStamp)
{
var security_stamp = subject.Claims.Where(c => c.Type == "security_stamp").Select(c => c.Value).SingleOrDefault();
if (security_stamp != null)
{
var db_security_stamp = await _userManager.GetSecurityStampAsync(user);
if (db_security_stamp != security_stamp)
return;
}
}
context.IsActive =
!user.LockoutEnabled ||
!user.LockoutEnd.HasValue ||
user.LockoutEnd <= DateTime.Now;
}
}
private IEnumerable<Claim> GetClaimsFromUser(ApplicationUser user)
{
var claims = new List<Claim>
{
new Claim(JwtClaimTypes.Subject, user.Id),
new Claim(JwtClaimTypes.PreferredUserName, user.UserName),
new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName)
};
if (!string.IsNullOrWhiteSpace(user.Name))
claims.Add(new Claim("name", user.Name));
if (!string.IsNullOrWhiteSpace(user.LastName))
claims.Add(new Claim("last_name", user.LastName));
if (_userManager.SupportsUserEmail)
{
claims.AddRange(new[]
{
new Claim(JwtClaimTypes.Email, user.Email),
new Claim(JwtClaimTypes.EmailVerified, user.EmailConfirmed ? "true" : "false", ClaimValueTypes.Boolean)
});
}
if (_userManager.SupportsUserPhoneNumber && !string.IsNullOrWhiteSpace(user.PhoneNumber))
{
claims.AddRange(new[]
{
new Claim(JwtClaimTypes.PhoneNumber, user.PhoneNumber),
new Claim(JwtClaimTypes.PhoneNumberVerified, user.PhoneNumberConfirmed ? "true" : "false", ClaimValueTypes.Boolean)
});
}
return claims;
}
}
我有一个 IdentityServer4 实例,我正试图在 nginx 代理后面的 Docker 容器中获取 运行ning。我基于 Git 存储库中的 AspNet 身份示例,但在用户成功注册新帐户后,我从 IdentityServer 获得 "An error occurred" 并且日志显示
[07:46:39 ERR] An unhandled exception has occurred: sub claim is missing
System.InvalidOperationException: sub claim is missing
at IdentityServer4.IdentityServerPrincipal.AssertRequiredClaims(ClaimsPrincipal principal
at IdentityServer4.Hosting.IdentityServerAuthenticationService.AugmentPrincipal(ClaimsPrincipal principal
at IdentityServer4.Hosting.IdentityServerAuthenticationService.<SignInAsync>d__7.MoveNext
我的Startup.cs看起来像这样
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().
var connectionString = Configuration.GetConnectionString("DefaultConnection");
var issuerUri = Configuration.GetSection("IssuerUri").Value;
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddTransient<IEmailSender, EmailSender>();
services.AddMvc();
services.AddCors(o => o.AddPolicy("CorsPolicy", b =>
{
b.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}));
services.AddIdentityServer(options =>
{
options.IssuerUri = issuerUri;
options.PublicOrigin = issuerUri;
})
.AddDeveloperSigningCredential()
// this adds the config data from DB (clients, resources)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
// this enables automatic token cleanup. this is optional.
//options.EnableTokenCleanup = true;
//options.TokenCleanupInterval = 30;
});
我一定是错过了一些明显的配置,但我看不到在哪里。有什么想法吗?
更新 我在这方面取得了一些进展,似乎已经克服了最初的错误。用户现已通过身份验证,但 signin-oidc 页面抛出错误
[11:33:21 INF] Request starting HTTP/1.1 POST http://mvcportal.co.uk/signin-oidc application/x-www-form-urlencoded 1565
[11:33:21 INF] AuthenticationScheme: Cookies signed in.
[11:33:21 INF] Request finished in 684.8425ms 302
[11:33:27 INF] Request starting HTTP/1.1 POST http://mvcportal.co.uk/signin-oidc application/x-www-form-urlencoded 1565
[11:33:27 ERR] Message contains error: 'invalid_grant', error_description: 'error_description is null', error_uri: 'error_uri is null', status code '400'.
我有一个有效的 JWT,但我注意到 idp 不等于发行者。对吗?
{
"nbf": 1508758474,
"exp": 1508758774,
"iss": "http://myproxiedlogonsitebehindnginx.co.uk",
"aud": "mvc.portal",
"nonce": "636443552746808541.MGVjMzk2NTEtYmYwNS00NmQwLTllOTQtZDVjNjdlYTA2YWVlYTQ3Zjg1NjgtZDA1Yi00NDE0LWJiYmYtMjM4YzI1NjZlYTcx",
"iat": 1508758474,
"c_hash": "kG7wG8vSgRe5zdriHQ6iMA",
"sid": "c9410ee8f27b69c32e43d5ac3d407f37",
"sub": "e80fb854-cab2-4381-8057-19de0fea73f4",
"auth_time": 1508757008,
"idp": "local",
"amr": [
"pwd"
]
}
更新 2 如果有帮助,这是 idsrv 上的客户端配置
new Client
{
ClientId = "mvc.portal",
ClientName = "Customer Portal",
ClientUri = customerPortalBaseUri,
ClientSecrets =
{
new Secret("21f51463-f436-4a84-92ce-1b520dd63a81".Sha256())
},
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
AllowAccessTokensViaBrowser = false,
RedirectUris = { $"{customerPortalBaseUri}/signin-oidc"},
FrontChannelLogoutUri = $"{customerPortalBaseUri}/signout-oidc",
PostLogoutRedirectUris = { $"{customerPortalBaseUri}/signout-callback-oidc" },
AllowOfflineAccess = true,
RequireConsent = false,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email
}
}
这是 client/portal 配置
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "oidc";
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "http://myproxiedlogonsitebehindnginx.co.uk";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc.portal";
options.ClientSecret = "21f51463-f436-4a84-92ce-1b520dd63a81";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
});
更新 3 所以现在我确信它与部署有关,因为如果我 运行 我本地机器上的 mvc 应用程序但使用容器中部署的 idsvr(在 nginx 后面)我可以毫无问题地进行身份验证,但是如果我尝试容器化门户的版本,我仍然会得到一个未处理的 500,但它没有被记录,然后如果我重试该操作,我会得到这个记录:
[11:22:51 INF] Request starting HTTP/1.1 POST http://mvcportal.co.uk/signin-oidc application/x-www-form-urlencoded 1559
[11:22:51 ERR] Message contains error: 'invalid_grant', error_description: 'error_description is null', error_uri: 'error_uri is null', status code '400'.
[11:22:51 ERR] Exception occurred while processing message.
Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectProtocolException: Message contains error: 'invalid_grant'], error_description: 'error_description is null', error_uri: 'error_uri is null'.
所以我终于弄清楚了。似乎 nginx 中的默认 header 限制不太好用,我在日志中发现了这一点
upstream sent too big header while reading response header from upstream
更新 nginx 配置以包含行
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
防止了 502 错误,现在所有验证都正常了。
感谢您的回复亲爱的丹, 另外,我得到了这个错误,我已经解决了这个问题
在 Startup.cs 配置服务中找到这一行
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores()
.AddIdentityServer()
.AddDefaultTokenProviders();
确保 .AddIdentityServer()
在那里。
我遇到了完全相同的问题并通过添加解决了它:
.AddAspNetIdentity<ApplicationUser>();
至
services.AddIdentityServer()
在Startup.cs
services.AddIdentityServer()
.AddAspNetIdentity<ApplicationUser>()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString,
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.MigrationsAssembly(migrationsAssembly);
});
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString,
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.MigrationsAssembly(migrationsAssembly);
});
}).Services.AddTransient<IProfileService, ProfileService>();
和
ProfileService.cs
public class ProfileService : IProfileService
{
private readonly UserManager<ApplicationUser> _userManager;
public ProfileService(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
async public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var subject = context.Subject ?? throw new ArgumentNullException(nameof(context.Subject));
var subjectId = subject.Claims.Where(x => x.Type == "sub").FirstOrDefault().Value;
var user = await _userManager.FindByIdAsync(subjectId);
if (user == null)
throw new ArgumentException("Invalid subject identifier");
var claims = GetClaimsFromUser(user);
context.IssuedClaims = claims.ToList();
}
async public Task IsActiveAsync(IsActiveContext context)
{
var subject = context.Subject ?? throw new ArgumentNullException(nameof(context.Subject));
var subjectId = subject.Claims.Where(x => x.Type == "sub").FirstOrDefault().Value;
var user = await _userManager.FindByIdAsync(subjectId);
context.IsActive = false;
if (user != null)
{
if (_userManager.SupportsUserSecurityStamp)
{
var security_stamp = subject.Claims.Where(c => c.Type == "security_stamp").Select(c => c.Value).SingleOrDefault();
if (security_stamp != null)
{
var db_security_stamp = await _userManager.GetSecurityStampAsync(user);
if (db_security_stamp != security_stamp)
return;
}
}
context.IsActive =
!user.LockoutEnabled ||
!user.LockoutEnd.HasValue ||
user.LockoutEnd <= DateTime.Now;
}
}
private IEnumerable<Claim> GetClaimsFromUser(ApplicationUser user)
{
var claims = new List<Claim>
{
new Claim(JwtClaimTypes.Subject, user.Id),
new Claim(JwtClaimTypes.PreferredUserName, user.UserName),
new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName)
};
if (!string.IsNullOrWhiteSpace(user.Name))
claims.Add(new Claim("name", user.Name));
if (!string.IsNullOrWhiteSpace(user.LastName))
claims.Add(new Claim("last_name", user.LastName));
if (_userManager.SupportsUserEmail)
{
claims.AddRange(new[]
{
new Claim(JwtClaimTypes.Email, user.Email),
new Claim(JwtClaimTypes.EmailVerified, user.EmailConfirmed ? "true" : "false", ClaimValueTypes.Boolean)
});
}
if (_userManager.SupportsUserPhoneNumber && !string.IsNullOrWhiteSpace(user.PhoneNumber))
{
claims.AddRange(new[]
{
new Claim(JwtClaimTypes.PhoneNumber, user.PhoneNumber),
new Claim(JwtClaimTypes.PhoneNumberVerified, user.PhoneNumberConfirmed ? "true" : "false", ClaimValueTypes.Boolean)
});
}
return claims;
}
}