如何添加要包含在令牌中的其他声明?
一旦API收到承载令牌,User.Identity对象就会填充以下声明.
[ { "key": "nbf","value": "1484614344" },{ "key": "exp","value": "1484615244" },{ "key": "iss","value": "http://localhost:85" },{ "key": "aud","value": "http://localhost:85/resources" },"value": "WebAPI" },{ "key": "client_id","value": "MyClient" },{ "key": "sub","value": "d74c815a-7ed3-4671-b4e4-faceb0854bf6" },{ "key": "auth_time","value": "1484611732" },{ "key": "idp","value": "local" },{ "key": "role","value": "AccountsManager" },{ "key": "scope","value": "openid" },"value": "profile" },"value": "roles" },"value": "offline_access" },{ "key": "amr","value": "pwd" } ]
我想要其他声明,如用户名,电子邮件,legacySystemUserId等.这些字段已经存在于AspNetUsers表中(并且不会重复存在于AspNetUserClaims表中),并且可以在我的ApplicationUser对象的ASP .Net Core应用程序中使用.
我希望它们包含在使用用户名和密码进行身份验证后返回的访问令牌中.想要在我无法访问身份服务器数据库的WebAPI应用程序中使用相同的内容,而且它自己的数据库根据用户的电子邮件地址而不是UserId存储数据(这是一个在ASP .NET身份中生成的guid并作为SUB索赔).
解决方法
几个小时以来我一直在打这个问题,最后拼凑出了解决方案.这个
article是一个很大的帮助,但总结和分享我的实现:
为了获得分配给用户的声明并将其附加到访问令牌,您需要在身份服务器上实现两个接口:IResourceOwnerPasswordValidator和IProfileService.以下是我对这两个类的实现,并且是粗略的草稿,但它们有效.
**请务必在此时获取最新版本的IdentityServer4 – 1.0.2.
public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator { private readonly UserManager<ApplicationUser> _userManager; public ResourceOwnerPasswordValidator(UserManager<ApplicationUser> userManager) { _userManager = userManager; } public Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { var userTask = _userManager.FindByNameAsync(context.UserName); var user = userTask.Result; context.Result = new GrantValidationResult(user.Id,"password",null,"local",null); return Task.FromResult(context.Result); } }
和
public class AspNetIdentityProfileService : IProfileService { private readonly UserManager<ApplicationUser> _userManager; public AspNetIdentityProfileService(UserManager<ApplicationUser> userManager) { _userManager = userManager; } public async Task GetProfileDataAsync(ProfileDataRequestContext context) { var subject = context.Subject; if (subject == null) throw new ArgumentNullException(nameof(context.Subject)); var subjectId = subject.GetSubjectId(); var user = await _userManager.FindByIdAsync(subjectId); if (user == null) throw new ArgumentException("Invalid subject identifier"); var claims = await GetClaimsFromUser(user); var siteIdClaim = claims.SingleOrDefault(x => x.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"); context.IssuedClaims.Add(new Claim(JwtClaimTypes.Email,user.Email)); context.IssuedClaims.Add(new Claim("siteid",siteIdClaim.Value)); context.IssuedClaims.Add(new Claim(JwtClaimTypes.Role,"User")); var roleClaims = claims.Where(x => x.Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"); foreach (var roleClaim in roleClaims) { context.IssuedClaims.Add(new Claim(JwtClaimTypes.Role,roleClaim.Value)); } } public async Task IsActiveAsync(IsActiveContext context) { var subject = context.Subject; if (subject == null) throw new ArgumentNullException(nameof(context.Subject)); var subjectId = subject.GetSubjectId(); 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 async Task<IEnumerable<Claim>> GetClaimsFromUser(ApplicationUser user) { var claims = new List<Claim> { new Claim(JwtClaimTypes.Subject,user.Id),new Claim(JwtClaimTypes.PreferredUserName,user.UserName) }; 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) }); } if (_userManager.SupportsUserClaim) { claims.AddRange(await _userManager.GetClaimsAsync(user)); } if (_userManager.SupportsUserRole) { var roles = await _userManager.GetRolesAsync(user); claims.AddRange(roles.Select(role => new Claim(JwtClaimTypes.Role,role))); } return claims; } }
一旦你有了这些,他们需要在startup.cs中添加到你的服务:
services.AddTransient<IResourceOwnerPasswordValidator,ResourceOwnerPasswordValidator>(); services.AddTransient<IProfileService,AspNetIdentityProfileService>();
这是我的配置快速查看:
public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { new IdentityResources.OpenId() }; } public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource { Name = "api1",Description = "My Api",Scopes = { new Scope() { Name = "api1",DisplayName = "Full access to Api" } } } }; } public static IEnumerable<Client> GetClients() { return new List<Client> { new Client { ClientId = "apiClient",ClientName = "Api Angular2 Client",AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,AlwaysSendClientClaims = true,AlwaysIncludeUserClaimsInIdToken = true,ClientSecrets = { new Secret("secret".Sha256()) },AllowedScopes = { "api1" } } }; }
之后,从客户端调用身份服务器:
var discoTask = DiscoveryClient.GetAsync("http://localhost:5000"); var disco = discoTask.Result; var tokenClient = new TokenClient(disco.TokenEndpoint,"apiClient","secret"); var tokenResponseTask = tokenClient.RequestResourceOwnerPasswordAsync("user@domain.com","my-password","api1"); var tokenResponse = tokenResponseTask.Result; var accessToken = tokenResponse.AccessToken; if (tokenResponse.IsError) { Console.WriteLine(tokenResponse.Error); return; }
检查jwt.io上的令牌并查看结果……