【问题标题】:sub claim is missing Identity Server 4 with Mongo DB子声明缺少带有 Mongodb 的 Identity Server 4
【发布时间】:2020-02-01 03:32:14
【问题描述】:

我正在使用 Identity Server 4 和 MongoDB 来创建身份门户。

services.AddIdentityServer().AddMongoRepository()
                .AddMongoDbForAspIdentity<ApplicationUser, IdentityRole>(Configuration)
                .AddClients()
                .AddIdentityApiResources()
                .AddPersistedGrants()
                .AddDeveloperSigningCredential();


app.UseMongoDbForIdentityServer();
            app.UseIdentityServer();

这里是 Mongo Db 存储库

namespace IdentityServer.Extension
{
    public static class IdentityServerBuilderExtensions
    {/// <summary>
     /// Adds mongo repository (mongodb) for IdentityServer
     /// </summary>
     /// <param name="builder"></param>
     /// <returns></returns>
        public static IIdentityServerBuilder AddMongoRepository(this IIdentityServerBuilder builder)
        {
            builder.Services.AddTransient<IRepository, Repository>();
            return builder;
        }

        /// <summary>
        /// Adds mongodb implementation for the "Asp Net Core Identity" part (saving user and roles)
        /// </summary>
        /// <remarks><![CDATA[
        /// Contains implemenations for
        /// - IUserStore<T>
        /// - IRoleStore<T>
        /// ]]></remarks>
        public static IIdentityServerBuilder AddMongoDbForAspIdentity<TIdentity, TRole>(this IIdentityServerBuilder builder, IConfigurationRoot configuration) where
            TIdentity : ApplicationUser where TRole : Microsoft.AspNetCore.Identity.MongoDB.IdentityRole
        {

            //User Mongodb for Asp.net identity in order to get users stored
            var configurationOptions = configuration.Get<MongoDbConfigurationOptions>();
            var client = new MongoClient(configurationOptions.MongoConnection);
            var database = client.GetDatabase(configurationOptions.MongoDatabaseName);

            // Configure Asp Net Core Identity / Role to use MongoDB
            builder.Services.AddSingleton<IUserStore<TIdentity>>(x =>
            {
                var usersCollection = database.GetCollection<TIdentity>("Identity_Users");
                IndexChecks.EnsureUniqueIndexOnNormalizedEmail(usersCollection);
                IndexChecks.EnsureUniqueIndexOnNormalizedUserName(usersCollection);
                return new UserStore<TIdentity>(usersCollection);
            });

            builder.Services.AddSingleton<IRoleStore<TRole>>(x =>
            {
                var rolesCollection = database.GetCollection<TRole>("Identity_Roles");
                IndexChecks.EnsureUniqueIndexOnNormalizedRoleName(rolesCollection);
                return new RoleStore<TRole>(rolesCollection);
            });
            builder.Services.AddIdentity<TIdentity, TRole>().AddDefaultTokenProviders();


            return builder;
        }

        /// <summary>
        /// Configure ClientId / Secrets
        /// </summary>
        /// <param name="builder"></param>
        /// <param name="configurationOption"></param>
        /// <returns></returns>
        public static IIdentityServerBuilder AddClients(this IIdentityServerBuilder builder)
        {
            builder.Services.AddTransient<IClientStore, CustomClientStore>();
            builder.Services.AddTransient<ICorsPolicyService, InMemoryCorsPolicyService>();
            return builder;
        }


        /// <summary>
        /// Configure API  &  Resources
        /// Note: Api's have also to be configured for clients as part of allowed scope for a given clientID 
        /// </summary>
        /// <param name="builder"></param>
        /// <returns></returns>
        public static IIdentityServerBuilder AddIdentityApiResources(this IIdentityServerBuilder builder)
        {
            builder.Services.AddTransient<IResourceStore, CustomResourceStore>();
            return builder;
        }

        /// <summary>
        /// Configure Grants
        /// </summary>
        /// <param name="builder">The builder.</param>
        /// <returns></returns>
        public static IIdentityServerBuilder AddPersistedGrants(this IIdentityServerBuilder builder)
        {
            builder.Services.TryAddSingleton<IPersistedGrantStore, CustomPersistedGrantStore>();
            return builder;
        }

    }
}

账户控制人

私有只读 SignInManager _signInManager; 私有只读 UserManager _userManager;

    public AccountController(
        IIdentityServerInteractionService interaction,
        IClientStore clientStore,
        IAuthenticationSchemeProvider schemeProvider,
        IEventService events, UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
    {
        _signInManager = signInManager;
        _userManager = userManager;
        _interaction = interaction;
        _clientStore = clientStore;
        _schemeProvider = schemeProvider;
        _events = events;
    }

if (ModelState.IsValid)
            {
                // This doesn't count login failures towards account lockout
                // To enable password failures to trigger account lockout, set lockoutOnFailure: true
                var result = await _signInManager.PasswordSignInAsync(model.UserName, model.Password,
                    model.RememberLogin, lockoutOnFailure: true); 
                if (result.Succeeded)
                {
                    var user = await _userManager.FindByNameAsync(model.UserName);
                    await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.Email, clientId: context?.ClientId));

                    // only set explicit expiration here if user chooses "remember me". 
                    // otherwise we rely upon expiration configured in cookie middleware.
                    AuthenticationProperties props = null;
                    if (AccountOptions.AllowRememberLogin && model.RememberLogin)
                    {
                        props = new AuthenticationProperties
                        {
                            IsPersistent = true,
                            ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
                        };
                    };

                    // issue authentication cookie with subject ID and username
                    await HttpContext.SignInAsync(user.Id, user.UserName, props);

                    if (context != null)
                    {
                        if (await _clientStore.IsPkceClientAsync(context.ClientId))
                        {
                            // if the client is PKCE then we assume it's native, so this change in how to
                            // return the response is for better UX for the end user.
                            return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
                        }

                        // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
                        return Redirect(model.ReturnUrl);
                    }

                    // request for a local page
                    if (Url.IsLocalUrl(model.ReturnUrl))
                    {
                        return Redirect(model.ReturnUrl);
                    }
                    else if (string.IsNullOrEmpty(model.ReturnUrl))
                    {
                        return Redirect("~/");
                    }
                    else
                    {
                        // user might have clicked on a malicious link - should be logged
                        throw new Exception("invalid return URL");
                    }
                }

                await _events.RaiseAsync(new UserLoginFailureEvent(model.UserName, "invalid credentials", clientId:context?.ClientId));
                ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
            }

运行这行代码时出现异常

var result = await _signInManager.PasswordSignInAsync(model.UserName, model.Password,model.RememberLogin, lockoutOnFailure: false);

异常错误

System.InvalidOperationException: sub claim is missing
   at IdentityServer4.Hosting.IdentityServerAuthenticationService.AssertRequiredClaims(ClaimsPrincipal principal)
   at IdentityServer4.Hosting.IdentityServerAuthenticationService.AugmentPrincipal(ClaimsPrincipal principal)
   at IdentityServer4.Hosting.IdentityServerAuthenticationService.SignInAsync(HttpContext context, String scheme, ClaimsPrincipal principal, AuthenticationProperties properties)
   at Microsoft.AspNetCore.Identity.SignInManager`1.SignInWithClaimsAsync(TUser user, AuthenticationProperties authenticationProperties, IEnumerable`1 additionalClaims)
   at Microsoft.AspNetCore.Identity.SignInManager`1.SignInOrTwoFactorAsync(TUser user, Boolean isPersistent, String loginProvider, Boolean bypassTwoFactor)
   at Microsoft.AspNetCore.Identity.SignInManager`1.PasswordSignInAsync(TUser user, String password, Boolean isPersistent, Boolean lockoutOnFailure)
   at Microsoft.AspNetCore.Identity.SignInManager`1.PasswordSignInAsync(String userName, String password, Boolean isPersistent, Boolean lockoutOnFailure)
   at IdentityServer.AccountController.Login(LoginInputModel model, String button) in /Users/macbook/Projects/IdentityPortal/IdentityServer/Quickstart/Account/AccountController.cs:line 116

【问题讨论】:

    标签: c# asp.net mongodb asp.net-identity identityserver4


    【解决方案1】:

    首先,当您向 indentityserver 添加客户端时,您必须给客户端 一些允许的范围,其中之一是 Profile:

     new Client
                    {
                        ClientId = "userjs",
                        ClientName = "",
                        AllowedGrantTypes = GrantTypes.Implicit,
                        AllowAccessTokensViaBrowser = true,
                        //ClientUri= $"{clientsUrl["UserSpa"]}",
                        RedirectUris =           { $"{clientsUrl["UserSpa"]}/" },
                        RequireConsent = false,
                        PostLogoutRedirectUris = { $"{clientsUrl["UserSpa"]}/" },
                        AllowedCorsOrigins =     { $"{clientsUrl["UserSpa"]}" },
                        AllowedScopes =
                        {
    
                            "club",
                            IdentityServerConstants.StandardScopes.OpenId,
                            **IdentityServerConstants.StandardScopes.Profile,**
                        },
                        RequireClientSecret = false
                    },  
    

    二、在IdentityServer之后添加AddProfileService扩展方法到服务中:

    services.AddIdentityServer().AddMongoRepository()
                    .AddMongoDbForAspIdentity<ApplicationUser, IdentityRole> 
                     (Configuration)
                    .AddClients()
                    .AddIdentityApiResources()
                    .AddPersistedGrants()
                    .AddDeveloperSigningCredential()
                    .AddProfileService<ProfileService>();
    

    三、实现IProfileService:

     public class ProfileService : IProfileService
        {
            private readonly UserManager<ApplicationUser> _userManager;
            private readonly IServiceCollection _services;
            private readonly ApplicationDbContext _context;
            private CalcAllowedPermissions _calcAllowedPermissions;
    
            public ProfileService(UserManager<ApplicationUser> userManager, ApplicationDbContext context)
            {
                _services = new ServiceCollection();
                var sp = _services.BuildServiceProvider();
                _userManager = userManager;
                _context = context ?? throw new ArgumentNullException(nameof(context));
                _calcAllowedPermissions = new CalcAllowedPermissions(_context);
            }
        **//This method is called whenever claims about the user are requested (e.g. during token creation or via the userinfo endpoint)**
            public async Task GetProfileDataAsync(ProfileDataRequestContext context)
            {
                var subject = context.Subject ?? throw new ArgumentNullException(nameof(context.Subject));
    
                var subjectId = subject.Claims.Where(x => x.Type == "sub").FirstOrDefault().Value;
    
                var user = await _userManager.FindByIdAsync(subjectId);
                if (user == null)
                    throw new ArgumentException("Invalid subject identifier");
    
                var claims = GetClaimsFromUser(user,subject);
                context.IssuedClaims = claims.Result.ToList();
            }
    
            public async Task IsActiveAsync(IsActiveContext context)
            {
                var subject = context.Subject ?? throw new ArgumentNullException(nameof(context.Subject));
    
                var subjectId = subject.Claims.Where(x => x.Type == "sub").FirstOrDefault().Value;
                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,ClaimsPrincipal subject)
            {
                var claims = new List<Claim>
                {
                    new Claim(JwtClaimTypes.Subject, user.Id),
                    new Claim(JwtClaimTypes.PreferredUserName, user.UserName),
                    new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName)
    
                };
    
                if (!string.IsNullOrWhiteSpace(user.Name))
                    claims.Add(new Claim("name", user.Name));
    
                if (!string.IsNullOrWhiteSpace(user.LastName))
                    claims.Add(new Claim("last_name", user.LastName));
                claims.Add(new Claim(PermissionConstants.PackedPermissionClaimType,
                   await _calcAllowedPermissions.CalcPermissionsForUserAsync(user.Id)));
    
                   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)
                    });
                }
    
                return claims;
            }
        }
    

    GetClaimsFromUser 方法是您应该添加声明的地方。

    【讨论】:

    • 您是否已将配置文件添加到允许的范围(如第一步)?
    • 是的,我已经按照您所说的方式实施了
    • 我必须登录才能提供 kudus,因为这节省了我研究的时间。谢谢!
    【解决方案2】:

    问题是我没有通过主题声明。

    public static List<TestUser> GetSampleUsers()
            {
                var subjectId = Guid.NewGuid().ToString();
                return new List<TestUser>
                {
                    new TestUser
                    {
                        Username = "admin@local.com",
                        Password = "RockStar.1",
    
                        Claims = new List<Claim>
                        {
                            new Claim(JwtClaimTypes.Name, "Admin "),
                            new Claim(JwtClaimTypes.GivenName, "Admin"),
                            new Claim(JwtClaimTypes.FamilyName, "add min"),
                            new Claim(JwtClaimTypes.Email, "admin@local.com"),
                            new Claim(JwtClaimTypes.Subject, subjectId) --> This solve the issue
                        }
                    }
                };
        }
    

    【讨论】:

      猜你喜欢
      • 2021-03-18
      • 2018-04-29
      • 1970-01-01
      • 2018-10-16
      • 1970-01-01
      • 2015-05-29
      • 1970-01-01
      • 1970-01-01
      • 2017-05-14
      相关资源
      最近更新 更多