如何使用带有 IdentityServer4 的 ASP.Net Identity 添加要包含在 access_token 中的其他声明 [英] How to add additional claims to be included in the access_token using ASP.Net Identity with IdentityServer4

查看:30
本文介绍了如何使用带有 IdentityServer4 的 ASP.Net Identity 添加要包含在 access_token 中的其他声明的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

如何添加要包含在令牌中的其他声明?

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

<预><代码>[{"key": "nbf",价值":1484614344"},{"key": "exp",价值":1484615244"},{"key": "iss",值":http://localhost:85"},{"key": "aud",值":http://localhost:85/resources"},{"key": "aud",值":WebAPI"},{"key": "client_id",价值":我的客户"},{"key": "子",值":d74c815a-7ed3-4671-b4e4-faceb0854bf6"},{"key": "auth_time",价值":1484611732"},{"key": "idp",值":本地"},{"key": "角色",值":帐户管理器"},{"key": "范围",值":openid"},{"key": "范围",价值":个人资料"},{"key": "范围",价值":角色"},{"key": "范围",值":WebAPI"},{"key": "范围",值":离线访问"},{"key": "amr",值":密码"}]

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

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

解决方案

我一直在为同样的问题解决几个小时,最后拼凑出解决方案.这个文章 是一个很大的帮助,但总结和分享我的实现:

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

**此时请务必获取最新版本的 IdentityServer4 - 1.0.2.

公共类ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator{private 只读 UserManager_userManager;public ResourceOwnerPasswordValidator(UserManager userManager){_userManager = 用户管理器;}公共任务 ValidateAsync(ResourceOwnerPasswordValidationContext 上下文){var userTask = _userManager.FindByNameAsync(context.UserName);var user = userTask.Result;context.Result = new GrantValidationResult(user.Id, "password", null, "local", null);返回 Task.FromResult(context.Result);}}

公共类 AspNetIdentityProfileService : IProfileService{private 只读 UserManager_userManager;公共 AspNetIdentityProfileService(UserManageruserManager){_userManager = 用户管理器;}公共异步任务 GetProfileDataAsync(ProfileDataRequestContext 上下文){var 主题 = context.Subject;if (subject == null) throw new ArgumentNullException(nameof(context.Subject));var subjectId = subject.GetSubjectId();var user = await _userManager.FindByIdAsync(subjectId);如果(用户==空)throw new ArgumentException("主题标识符无效");var claim = await GetClaimsFromUser(user);var siteIdClaim = claim.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 = claim.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));}}公共异步任务 IsActiveAsync(IsActiveContext 上下文){var 主题 = context.Subject;if (subject == null) throw new ArgumentNullException(nameof(context.Subject));var subjectId = subject.GetSubjectId();var user = await _userManager.FindByIdAsync(subjectId);context.IsActive = false;如果(用户!= 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);如果(db_security_stamp != security_stamp)返回;}}上下文.IsActive =!user.LockoutEnabled ||!user.LockoutEnd.HasValue ||user.LockoutEnd <= DateTime.Now;}}私有异步任务<IEnumerable<Claim>>GetClaimsFromUser(ApplicationUser 用户){var claim = new List{新索赔(JwtClaimTypes.Subject,user.Id),新索赔(JwtClaimTypes.PreferredUserName,user.UserName)};如果 (_userManager.SupportsUserEmail){索赔.AddRange(新[]{新索赔(JwtClaimTypes.Email,user.Email),新索赔(JwtClaimTypes.EmailVerified,user.EmailConfirmed?真":假",ClaimValueTypes.Boolean)});}if (_userManager.SupportsUserPhoneNumber && !string.IsNullOrWhiteSpace(user.PhoneNumber)){索赔.AddRange(新[]{新索赔(JwtClaimTypes.PhoneNumber,user.PhoneNumber),新索赔(JwtClaimTypes.PhoneNumberVerified,user.PhoneNumberConfirmed?真":假",ClaimValueTypes.Boolean)});}如果(_userManager.SupportsUserClaim){claim.AddRange(await _userManager.GetClaimsAsync(user));}如果(_userManager.SupportsUserRole){var 角色 = await _userManager.GetRolesAsync(user);claim.AddRange(roles.Select(role => new Claim(JwtClaimTypes.Role, role)));}退货索赔;}}

一旦有了这些,就需要将它们添加到您在 startup.cs 中的服务中:

services.AddTransient();services.AddTransient();

这是我的配置的快速浏览:

public static IEnumerable获取身份资源(){返回新列表{新的 IdentityResources.OpenId()};}公共静态 IEnumerableGetApiResources(){返回新列表{新的 API 资源{名称 = "api1",描述 = "我的 API",范围 ={新范围(){名称 = "api1",DisplayName = "完全访问 Api"}}}};}公共静态 IEnumerable获取客户端(){返回新列表<客户端>{新客户{ClientId = "apiClient",ClientName = "Api Angular2 客户端",AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,AlwaysSendClientClaims = true,AlwaysIncludeUserClaimsInIdToken = true,客户机密 ={新的秘密(秘密".Sha256())},允许范围 ={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;如果(令牌响应.IsError){Console.WriteLine(tokenResponse.Error);返回;}

在 jwt.io 检查令牌并查看结果...

How to add additional claims to be included within the token?

As soon as the API receives the bearer token, the User.Identity object gets populated with the following claims.

[
  {
    "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"
  }
]

I want additional claims like username, email, legacySystemUserId, etc. These fields already exist in the AspNetUsers table (and doesn't repetitively exist in AspNetUserClaims table) and are available in ASP .Net Core application in my ApplicationUser object.

I want them to be included in access token that is returned after authenticating with username and password. Want to use the same in my WebAPI application that doesn't have access to the identity-server database and its own database has data stored based on user's email address not the UserId (which is a guid generated in ASP .NET Identity and received as SUB claim).

解决方案

I had been fighting this same issue for hours and finally pieced together the solution. This article was a big help, but to summarize and share my implementation:

In order to get the claims assigned to the user and attach them to the access token, you need to implement two interfaces on the identity server: IResourceOwnerPasswordValidator and IProfileService. The following are my implementations of the two classes and are rough drafts, but they work.

**Be sure to get the latest version of IdentityServer4 - 1.0.2 at this time.

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);
    }
}

and

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;
    }
}

Once you have those, they need to be added to your services in startup.cs:

services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();
services.AddTransient<IProfileService, AspNetIdentityProfileService>();

Here is a quick look at my config:

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"
            }
        }
    };
}

After that, a call to the identity server from a client:

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;
}

Inspect the token at jwt.io and see your results...

这篇关于如何使用带有 IdentityServer4 的 ASP.Net Identity 添加要包含在 access_token 中的其他声明的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆