【问题标题】:How to add additional claims to be included in the access_token using ASP.Net Identity with IdentityServer4如何使用带有 IdentityServer4 的 ASP.Net Identity 添加附加声明以包含在 access_token 中
【发布时间】:2017-05-31 22:57:49
【问题描述】:

如何添加额外的声明以包含在令牌中?

一旦 API 收到不记名令牌,User.Identity 对象就会填充以下声明。

[
  {
    "key": "nbf",
    "value": "1484614344"
  },
  {
    "key": "exp",
    "value": "1484615244"
  },
  {
    "key": "iss",
    "value": "http://localhost:85"
  },
  {
    "key": "aud",
    "value": "http://localhost:85/resources"
  },
  {
    "key": "aud",
    "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"
  },
  {
    "key": "scope",
    "value": "profile"
  },
  {
    "key": "scope",
    "value": "roles"
  },
  {
    "key": "scope",
    "value": "WebAPI"
  },
  {
    "key": "scope",
    "value": "offline_access"
  },
  {
    "key": "amr",
    "value": "pwd"
  }
]

我想要额外的声明,例如username, email, legacySystemUserId 等。这些字段已经存在于AspNetUsers 表中(并且不会重复存在于AspNetUserClaims 表中),并且在我的 ApplicationUser 对象中的 ASP .Net Core 应用程序中可用.

我希望它们包含在使用用户名和密码进行身份验证后返回的访问令牌中。想在我的 WebAPI 应用程序中使用相同的内容,该应用程序无法访问身份服务器数据库,并且它自己的数据库存储的数据基于用户的电子邮件地址而不是 UserId(这是在 ASP .NET Identity 中生成的 guid 并接收为SUB 声明)。

【问题讨论】:

    标签: asp.net-core identityserver4 asp.net-core-webapi asp.net-core-identity


    【解决方案1】:

    我已经为同一个问题解决了几个小时,最终拼凑出解决方案。这个article 帮了大忙,但总结和分享我的实现:

    为了获取分配给用户的声明并将它们附加到访问令牌,您需要在身份服务器上实现两个接口:IResourceOwnerPasswordValidatorIProfileService。以下是我对这两个类的实现,是粗略的草稿,但它们确实有效。

    **此时务必获取最新版本的 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 检查令牌并查看结果...

    【讨论】:

    • 这正是我遇到的问题,只是你的解决方案似乎不起作用,因为这两个新类中的代码永远不会被调用。它们甚至从未建造过。我不确定我错过了什么。有什么建议吗?
    • @Phileosophos 在当前版本(截至今天为 4.1.1)中,您应该像这样在 Startup.cs 中添加您的 IProfileService,您会发现它确实被框架调用:@987654329 @ 如果您不想,也不需要包含 IResourceOwnerPasswordValidator 的自定义实现。
    猜你喜欢
    • 1970-01-01
    • 2020-06-16
    • 1970-01-01
    • 2020-03-27
    • 2017-11-16
    • 1970-01-01
    • 2016-10-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多