簡體   English   中英

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

[英]How does Asp.Net Identity 2 User Info get mapped to IdentityServer3 profile claims

我已經通過 Dapper 通過 SQL Server 支持的自定義用戶存儲設置並運行了 Asp.Net Identity 2。 此時在我的開發/測試中,我只關心本地帳戶(但會添加外部登錄提供程序)。 我有一個自定義用戶,其中包含 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如何將AspNetIdentityUserServiceUserName屬性AppUserpreferred_username聲明,我不確定這是如何發生的。 因此,我不明白如何將FirstName屬性映射到given_name聲明,以便它將與id_token一起返回。

我研究的內容:

因此,如果您在這里查看IdentityServer3 AspNetIdentity 示例,我發現這個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 自定義用戶示例,我發現這個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標志的配置文件范圍定義自己的定義,或者是否有辦法將該標志添加到標准范圍。

並添加到所有這些......這僅需要在使用本地帳戶時完成。 當我實現外部登錄提供程序時,我會在那里詢問配置文件,並希望能夠獲得名字和姓氏。 所以我想知道一旦我已經得到這些聲明(或者如何確定我是否需要從我的用戶存儲中提取它們)會發生什么。 似乎我需要連接到僅在進行本地登錄時運行的東西。

然后我開始真正質疑我是否以正確的方式解決這個問題,因為我看到/找到的信息很少(我原以為這是其他人已經實施的相當普遍的情況,並希望查找文檔/示例)。 一直試圖解決這個問題一天了。 希望有人有一個快速的答案/指針!

我使用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());
        }
    }
}

這個問題的 (A) 正確答案是覆蓋AspNetIdentityUserService類中的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;
            }
        }
    }
}

但正如我所發現的,這還不夠。 查看 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不會調用GetProfileDataAsync (不確定他們為什么選擇這樣做,但顯然必須有充分的理由!)。 所以我認為這意味着我需要完全重新定義profile范圍,但是通過進一步挖掘源代碼,我發現情況並非如此。 StandardScopes 有一個方法可以創建帶有 always include 標志 set 的范圍 而不是設置你的范圍這樣做:

        factory.UseInMemoryScopes(StandardScopes.All);

做這個:

        factory.UseInMemoryScopes(StandardScopes.AllAlwaysInclude);

然后您的GetProfileDataAsync將運行,您將獲得所有聲明!

注意:我第一次嘗試使用ClaimsIdentityFactory並沒有成功,因為我沒有登錄到 Asp.Net Identity,而且除非我這樣做,否則它永遠不會被調用是有道理的。

注意:@Rosdi Kasim 的回答如果您希望在已經從 Identity Server 收到您的 id_token 之后添加聲明(尤其是應用程序特定聲明)當然是有效的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM