Asp.Net Identity 2用户信息如何映射到IdentityServer3配置文件声明

时间:2019-01-31 01:15:10

标签: c# openid-connect identityserver3 asp.net-identity-2

我已经设置好了Asp.Net Identity 2,并使用SQL Server通过Dapper支持的自定义用户存储来运行。此时,在我的开发/测试中,我只关心本地帐户(但将添加到外部登录提供程序中)。我有一个自定义用户,其中包含Asp.Net Identity所需的标准属性,并添加了我自己的一些属性(名字,姓氏):

public class AppUser : IUser<Guid>
{
    public Guid Id { get; set; }
    public string UserName { get; set; }
    public string PasswordHash { get; set; }
    public string SecurityStamp { get; set; }
    public string Email { get; set; }
    public bool EmailConfirmed { get; set; }
    public bool LockoutEnabled { get; set; }
    public DateTimeOffset LockoutEndDate { get; set; }
    public int AccessFailedCount { get; set; }

    // Custom User Properties
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

在我的MVC Web应用程序中,我像这样配置OIDC:

       app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
        {
            Authority = ConfigurationManager.AppSettings["OpenIdConnectAuthenticationOptions.Authority"],
            ClientId = "MVC.Web",
            Scope = "openid profile email",
            RedirectUri = ConfigurationManager.AppSettings["OpenIdConnectAuthenticationOptions.RedirectUri"],
            ResponseType = "id_token",
            SignInAsAuthenticationType = "Cookies"
        });

由于我将profile作为要求的范围,因此我得到:

preferred_username: testuser

而且由于我将email包括在要求的范围内,因此我得到:

email:          user@test.com
email_verified: true

我没有明确告诉我的AspNetIdentityUserService如何将我的UserName中的AppUser属性映射到preferred_username声明,并且不确定如何发生。 因此,我不明白如何将FirstName属性映射到given_name声明,以便将其与id_token一起返回。

我研究的内容:

因此,如果您查看IdentityServer3 AspNetIdentity sample here,我发现这个ClaimsIdentityFactory看起来应该可以解决问题:

    public override async Task<ClaimsIdentity> CreateAsync(UserManager<User, string> manager, User user, string authenticationType)
    {
        var ci = await base.CreateAsync(manager, user, authenticationType);
        if (!String.IsNullOrWhiteSpace(user.FirstName))
        {
            ci.AddClaim(new Claim("given_name", user.FirstName));
        }
        if (!String.IsNullOrWhiteSpace(user.LastName))
        {
            ci.AddClaim(new Claim("family_name", user.LastName));
        }
        return ci;
    }

因此,我将其添加到了我的应用中,并使用自定义UserManager将其连接起来。而且我在实例化类时确实遇到了一个断点,但是我从来没有在CreateAsync方法上遇到过断点,并且我的声明也没有返回。

我也看到了这个IdentityServer3 Custom User sample here,并且发现了这个GetProfileDataAsync方法看起来似乎是正确的事情(但似乎我在某种程度上似乎要比我更深入地研究)简单/常见):

    public override Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        // issue the claims for the user
        var user = Users.SingleOrDefault(x => x.Subject == context.Subject.GetSubjectId());
        if (user != null)
        {
            context.IssuedClaims = user.Claims.Where(x => context.RequestedClaimTypes.Contains(x.Type));
        }

        return Task.FromResult(0);
    }

我在这里遇到了同样的问题,因为这种方法的断点从未触发。我什至甚至查看了IdentityServer3源代码,并且看到只有在范围设置了IncludeAllClaimsForUser标志的情况下才调用此源代码。但是我在这里使用标准的profile范围,所以我开始质疑是否需要为设置了IncludAllClaimsForUser标志的配置文件范围定义自己的定义,或者是否有办法将该标志添加到标准范围。

要添加到所有这些内容中...仅在使用本地帐户时才需要这样做。当我实现外部登录提供程序时,我将在那里要求配置文件,并希望能够获得名字和姓氏。因此,我想知道一旦获得了这些声明(或者如何确定是否需要从用户存储中提取它们),会发生什么情况。似乎我需要挂接到仅在进行本地登录时才能运行的功能。

然后,我开始真正地质疑我是否要以正确的方式进行操作,因为我看到/发现的信息很少(我希望这是其他人已经实现的相当普遍的情况,并希望找到文档/样本)。一直在尝试解决这一问题。希望有人有一个快速的答案/指针!

2 个答案:

答案 0 :(得分:1)

我使用OpenIdConnectAuthenticationNotifications来实现这一点,您可以连接到ASP.NET Identity数据库或在其中进行任何操作,这是我用于我的一个项目的示例代码:

这是我Startup.cs中的完整源代码,但您真正需要的只是SecurityTokenValidated部分...

using System.Configuration;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web.Helpers;
using IdentityServer3.Core;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;

namespace MyProject
{
    public partial class Startup
    {
        public static string AuthorizationServer => ConfigurationManager.AppSettings["security.idserver.Authority"];

        public void ConfigureOAuth(IAppBuilder app)
        {
            AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;

            var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
            jwtSecurityTokenHandler.InboundClaimTypeMap.Clear();

            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = "Cookies"
            });

            app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
            {
                SecurityTokenValidator = jwtSecurityTokenHandler,
                Authority = AuthorizationServer,
                ClientId = ConfigurationManager.AppSettings["security.idserver.clientId"],
                PostLogoutRedirectUri = ConfigurationManager.AppSettings["security.idserver.postLogoutRedirectUri"],
                RedirectUri = ConfigurationManager.AppSettings["security.idserver.redirectUri"],
                ResponseType = ConfigurationManager.AppSettings["security.idserver.responseType"],
                Scope = ConfigurationManager.AppSettings["security.idserver.scope"],
                SignInAsAuthenticationType = "Cookies",
#if DEBUG
                RequireHttpsMetadata = false,   //not recommended in production
#endif
                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    RedirectToIdentityProvider = n =>
                    {
                        if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
                        {
                            var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");

                            if (idTokenHint != null)
                            {
                                n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
                            }
                        }

                        return Task.FromResult(0);
                    },

                    SecurityTokenValidated = n =>
                    {
                        var id = n.AuthenticationTicket.Identity;

                        //// we want to keep first name, last name, subject and roles
                        //var givenName = id.FindFirst(Constants.ClaimTypes.GivenName);
                        //var familyName = id.FindFirst(Constants.ClaimTypes.FamilyName);
                        //var sub = id.FindFirst(Constants.ClaimTypes.Subject);
                        //var roles = id.FindAll(Constants.ClaimTypes.Role);

                        //// create new identity and set name and role claim type
                        var nid = new ClaimsIdentity(
                            id.AuthenticationType,
                            Constants.ClaimTypes.Name,
                            Constants.ClaimTypes.Role);

                        nid.AddClaims(id.Claims);
                        nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
                        nid.AddClaim(new Claim("access_Token", n.ProtocolMessage.AccessToken));

                        ////nid.AddClaim(givenName);
                        ////nid.AddClaim(familyName);
                        ////nid.AddClaim(sub);
                        ////nid.AddClaims(roles);

                        ////// add some other app specific claim
                        // Connect to you ASP.NET database for example
                        ////nid.AddClaim(new Claim("app_specific", "some data"));

                        //// keep the id_token for logout
                        //nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));

                        n.AuthenticationTicket = new AuthenticationTicket(
                            nid,
                            n.AuthenticationTicket.Properties);

                        return Task.FromResult(0);
                    }
                }
            });

            //app.UseResourceAuthorization(new AuthorizationManager());
        }
    }
}

答案 1 :(得分:0)

此问题的(A)正确答案是像这样覆盖GetProfileDataAsync类中的AspNetIdentityUserService方法:

public class AppUserService : AspNetIdentityUserService<AppUser, Guid>
{
    private AppUserManager _userManager;

    public AppUserService(AppUserManager userManager)
        : base(userManager)
    {
        _userManager = userManager;
    }

    public async override Task GetProfileDataAsync(ProfileDataRequestContext ctx)
    {
        var id = Guid.Empty;
        if (Guid.TryParse(ctx.Subject.GetSubjectId(), out id))
        {
            var user = await _userManager.FindByIdAsync(id);
            if (user != null)
            {
                var claims = new List<Claim>
                {
                    new Claim(Constants.ClaimTypes.PreferredUserName, user.UserName),
                    new Claim(Constants.ClaimTypes.Email, user.Email),
                    new Claim(Constants.ClaimTypes.GivenName, user.FirstName),
                    new Claim(Constants.ClaimTypes.FamilyName, user.LastName)
                };
                ctx.IssuedClaims = claims;
            }
        }
    }
}

但是,正如我发现的那样,这还不够。查看source code for IdentityServer,您会发现以下内容:

        if (scopes.IncludesAllClaimsForUserRule(ScopeType.Identity))
        {
            Logger.Info("All claims rule found - emitting all claims for user.");

            var context = new ProfileDataRequestContext(
                subject,
                client,
                Constants.ProfileDataCallers.ClaimsProviderIdentityToken);

            await _users.GetProfileDataAsync(context);

            var claims = FilterProtocolClaims(context.IssuedClaims);
            if (claims != null)
            {
                outputClaims.AddRange(claims);
            }

            return outputClaims;
        }

请注意,除非设置了一个包含所有声明的标志,否则GetProfileDataAsync不会被调用(不确定为什么他们选择这样做,但是显然必须有充分的理由!)。因此,我认为这意味着我需要完全重新定义profile范围,但是通过进一步挖掘源代码,我发现并非如此。 StandardScopes has a method that creates the scopes with the always include flag set。而不是设置您的范围:

        factory.UseInMemoryScopes(StandardScopes.All);

执行以下操作:

        factory.UseInMemoryScopes(StandardScopes.AllAlwaysInclude);

您的GetProfileDataAsync将运行后,您将获得所有索赔!

注意:我第一次尝试使用ClaimsIdentityFactory不会成功,因为我没有登录到Asp.Net Identity,因此除非这样,否则永远不会调用它,这是有道理的我在做什么。

注意:如果您希望在已从Identity Server收到id_token之后添加声明(尤其是特定于应用程序的声明),则@Rosdi Kasim的答案肯定有效。