What is missing from my configuration of Identity's UserManager and SignInManager with custom data providers?

-1

I am building a relatively simple line of business application using .Net Core Razor Pages. There is only one table for the application data in the database and eight razor pages total. I used Dapper as my ORM. I need role based authorization for the application: One for a manager, who can create, see, edit, and delete records. One for workers who can see records and edit one field of each particular record. One for an administrator who can add, edit, and delete users and roles. Knowing that EF is default for using Identity, I did some research and made custom storage providers as outlined here:

https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity-custom-storage-providers?view=aspnetcore-2.2

and here:

https://markjohnson.io/articles/asp-net-core-identity-without-entity-framework/

public class UserStore : IUserStore<User>, IUserPasswordStore<User>
    {
        public async Task<IdentityResult> CreateAsync(User user, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var accessor = new UserDataAccess();

            int rows = await accessor.InsertUser(user);

            if (rows > 0)
            {
                return IdentityResult.Success;
            }
            else
            {
                return IdentityResult.Failed(new IdentityError { Description = $"ERROR: Could not create new user {user.UserName}" });
            }
        }

        public async Task<IdentityResult> DeleteAsync(User user, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var accessor = new UserDataAccess();

            int rows = await accessor.DeleteUser(user);

            if (rows > 0)
            {
                return IdentityResult.Success;
            }
            else
            {
                return IdentityResult.Failed(new IdentityError { Description = $"ERROR: Could not delete user {user.UserName}" });
            }
        }

        public async Task<User> FindByIdAsync(string userId, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var accessor = new UserDataAccess();

            var selectedUser = await accessor.SelectUser(System.Convert.ToInt32(userId));

            return selectedUser;           
        }

        public async Task<IdentityResult> UpdateAsync(User user, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var accessor = new UserDataAccess();

            int rows = await accessor.UpdateUser(user);

            if (rows > 0)
            {
                return IdentityResult.Success;
            }
            else
            {
                return IdentityResult.Failed(new IdentityError { Description = $"ERROR: Could not change user data for {user.UserName}" });
            }
        }

        public async Task<User> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var accessor = new UserDataAccess();

            var userResult = await accessor.SelectUserByName(normalizedUserName);

            return userResult;
        }

        public Task<string> GetNormalizedUserNameAsync(User user, CancellationToken cancellationToken)
        {
            return Task.FromResult(user.UserName);
        }

        public Task<string> GetUserIdAsync(User user, CancellationToken cancellationToken)
        {
            return Task.FromResult(user.Id.ToString());
        }

        public Task<string> GetUserNameAsync(User user, CancellationToken cancellationToken)
        {
            return Task.FromResult(user.UserName);
        }

        public Task SetNormalizedUserNameAsync(User user, string normalizedName, CancellationToken cancellationToken)
        {
            user.UserName = normalizedName;
            return Task.FromResult(0);
        }

        public Task SetUserNameAsync(User user, string userName, CancellationToken cancellationToken)
        {
            user.UserName = userName;
            return Task.FromResult(0);
        }



        #region IDisposable Support
        private bool disposedValue = false; // To detect redundant calls

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    // TODO: dispose managed state (managed objects).
                }

                // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
                // TODO: set large fields to null.

                disposedValue = true;
            }
        }

        // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
        // ~UserStore()
        // {
        //   // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
        //   Dispose(false);
        // }

        // This code added to correctly implement the disposable pattern.
        public void Dispose()
        {
            // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
            Dispose(true);
            // TODO: uncomment the following line if the finalizer is overridden above.
            // GC.SuppressFinalize(this);
        }

        public Task SetPasswordHashAsync(User user, string passwordHash, CancellationToken cancellationToken)
        {
            user.PasswordHash = passwordHash;
            return Task.FromResult(0);
        }

        public Task<string> GetPasswordHashAsync(User user, CancellationToken cancellationToken)
        {

            return Task.FromResult(user.PasswordHash);
        }

        public Task<bool> HasPasswordAsync(User user, CancellationToken cancellationToken)
        {
            return Task.FromResult(user.PasswordHash != null);
        }

I also made a similar store for Roles.

I wired them up at startup thus:

        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddTransient<IUserStore<User>, UserStore>();
            services.AddTransient<IRoleStore<Role>, RoleStore>();
            services.AddTransient<IUserRoleStore<UserRole>, UserRoleStore>();

            services.AddIdentity<User, Role>().AddDefaultTokenProviders()
                                              .AddUserStore<UserStore>()
                                              .AddRoleStore<RoleStore>()
                                              .AddUserManager<User>()
                                              .AddRoleManager<Role>()
                                              .AddSignInManager();

            services.AddAuthentication(options =>
            {
                options.DefaultScheme = IdentityConstants.ApplicationScheme;
            });
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseCookiePolicy();
            app.UseAuthentication();
            app.UseMvc();
        }

Then I call them from my CreateUserPageModel

        public string UserName { get; set; }
        public string PasswordHash { get; set; }
        private readonly UserManager<User> _userManager;

        public CreateUserModel(UserManager<User> userManager)
        {
            _userManager = userManager;
        }

        public void OnGet()
        {

        }

        public async Task<IActionResult> OnPost()
        {
            var newUser = new User();
            newUser.UserName = UserName;
            newUser.PasswordHash = PasswordHash;

            var creationResult = await _userManager.CreateAsync(newUser, PasswordHash);

            if (creationResult.Succeeded)
            {
                return this.Page();
            }
            else
            {
                return Redirect("/Error");
            }
        }

As configured above, the application always redirects to the error page. When debugging, I see that my creationResult is always null. I suspect that my _userManager is also null here. I get similar null errors when I try to use the SignInManager the same way on a login page (again injecting through the constructor - ommitted here for brevity).

I haven't had luck trying to wire up the UserManager at startup using DI either, when changing the registration of Identity services in the startup class to this:

            services.AddIdentity<User, Role>().AddDefaultTokenProviders()
                                              .AddUserStore<UserStore>()
                                              .AddRoleStore<RoleStore>()
                                              .AddUserManager<User>()
                                              .AddRoleManager<Role>()
                                              .AddSignInManager();

I receive a System.InvalidOperationException at startup. And the app doesn't run at all.

System.InvalidOperationException
HResult=0x80131509
Message=Type User must derive from UserManager.
Source=Microsoft.Extensions.Identity.Core

This exception makes no sense to me if my User class is just supposed to encapsulate the properties of the app's users. I also don't understand why this is so if the layering works as the MS documentation cited above diagrams: the UserManager interacts with the UserStore, which interacts with my DataAccess class, which interacts with my Database.

I fear that I am missing something obvious here, either on startup or in implementing a custom UserManager, but I've seen other examples of working applications here and on GitHub that basically do the same thing, and can't figure out what this app lacks that they have.

c#
asp.net-core
dapper
razor-pages
asp.net-core-identity
asked on Stack Overflow Feb 5, 2020 by Lox Ness Monster • edited Feb 6, 2020 by Lox Ness Monster

0 Answers

Nobody has answered this question yet.


User contributions licensed under CC BY-SA 3.0