Search code examples
c#asp.net-mvcasp.net-coreasp.net-identity.net-standard

Identity in ASP.NET MVC Framework using Identity Core


I am having trouble with switching partially to .NET Standard.

I am in the process of migrating a class library to .NET Standard, in this library I have the repositories and the database communication. I already migrated this successfully to use AspNetCore.Identity.EntityFrameworkCore. What I try to achieve is eventually having 1 .NET Standard project taking care of the database, where 1 MVC .NET Framework, 1 API .NET Framework and 1 new .NET Core application will be using it. Besides that a few other .NET Framework class libraries depend on it. Basically the .NET Core application has been made already but the back-end has not been 'merged' for overlapping functionalities.

Little overview:

overview

Reason for not converting MVC/API to .Core is that too many other libraries currently depend on .NET Framework, some are not yet convertible, but using the same library for the database is a fundamental change which will avoid double implementations of some repositories.

I have also already converted my entities that implement Microsoft.AspNetCore.Identity.IdentityUser, Microsoft.AspNetCore.Identity.IdentityRole, etc. So my DbContext class looks like this:

public class DatabaseContext : IdentityDbContext<ApplicationUser, ApplicationRole, string, ApplicationUserClaim, ApplicationUserRole, ApplicationUserLogin, ApplicationRoleClaim, ApplicationUserToken>
{
    private IConfiguration _config;
    public DatabaseContext(IConfiguration config) : base()
    {
        _config = config;
    }
    //all DbSet, OnModelCreating
}

I have successfully ran the EFCore Code First Migrations.

Now I am trying to configure the Identity in my MVC application (and then in the API project as well).

I just have the standard IdentityConfig.cs, Startup.Auth.cs where all the configuration is done. I have tried looking at this documentation (migration identity). All I could do is add this, where AddMvc() does not exist so that throws an compile error:

Startup.cs

using System;
using System.IO;
using Babywatcher.Core.Data.Database;
using Babywatcher.Core.Data.Entities;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Owin;
using Owin;

[assembly: OwinStartupAttribute(typeof(MyProject.MVC.Startup))]
namespace MyProject.MVC
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            var services = new ServiceCollection();
            ConfigureAuth(app);
            ConfigureServices(services);
        }

        public void ConfigureServices(IServiceCollection services)
        {
            // Add EF services to the services container.
            services.AddDbContext<DatabaseContext>(options =>
                options.UseSqlServer(""));//Configuration trying to refer to above method: Configuration.GetConnectionString("DefaultConnection")

            services.AddIdentity<ApplicationUser, ApplicationRole>()
                .AddEntityFrameworkStores<DatabaseContext>()
                .AddDefaultTokenProviders();

            services.AddMvc();
        }
    }
}

Well I guess this doesn't really do much, Also I am using SimpleInjector throughout both .NET Framework projects which I would prefer to keep using if possible in stead of going to use the default dependency injector.

By adding above I don't really know what to do with the ConfigureAuth method and where to put all the configuration.

When I try to adjust the IdentityManager, to try and reference the same types within AspNetCore.Identity I start to get issues when trying to change the ApplicationUserManager:

ApplicationUserManager

Creating the UserStore is not a problem, but trying to create the UserManager is more difficult, I also have tried to use this in my UserRepository, like I did before, which I can't use now anymore :(.

Old UserRepository

private readonly UserManager<ApplicationUser, string> _userManager = null;
private readonly RoleManager<ApplicationRole, string> _roleManager = null;

internal static IDataProtectionProvider DataProtectionProvider { get; private set; }
public UserRepository(DatabaseContext dbContext) : base(dbContext, c => c.contactId, m => m.ContactId)
{
    var userStore =
        new UserStore<ApplicationUser, ApplicationRole, string, ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim>(dbContext);
    var roleStore = new RoleStore<ApplicationRole, string, ApplicationUserRole>(dbContext);
    _userManager = new UserManager<ApplicationUser, string>(userStore);
    _roleManager = new RoleManager<ApplicationRole, string>(roleStore);
    _userManager.UserValidator = new UserValidator<ApplicationUser>(_userManager) { AllowOnlyAlphanumericUserNames = false };
    if (DataProtectionProvider == null)
    {
        DataProtectionProvider = new MachineKeyProtectionProvider();
    }
    _userManager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser, string>(DataProtectionProvider.Create("Identity"));
}

Attempting to do above again gives issues when creating the UserManager because of the 9 arguments it asks so I kind-of felt that this shouldn't be done this way... Probably going to do something like this to get them there of course I first want to fix the root problem.

Last but not least: Keep in mind that these applications are all in production already, so especially around the users I need to make sure that the logins all still work. When bringing this version live I will need to migrate the data to a new database because of some other migration issues.


Solution

  • Alright so this is a long answer, be prepared to spent the next day on pulling your hair out of your head :).

    First an interface of IApplicationUser which is implemented by 2 different classes:

    public interface IApplicationUser
    {
        string Id { get; set; }
    
        /// <summary>Gets or sets the user name for this user.</summary>
        string UserName { get; set; }
    }
    

    Implementation 1, this one is part of my database entities:

    public class ApplicationUser :  IdentityUser<string>, IApplicationUser
    {
        public DateTime? LockoutEndDateUtc { get; set; }
        public bool RequiresPasswordCreation { get; set; }
        public string TemporaryToken { get; set; }
    }
    

    Implementation 2, for my .NET Framework projects:

    public class ApplicationUserMvc : IdentityUser<string, ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim>, IApplicationUser
    {
        public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUserMvc, string> manager)
        {
            // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
            var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
            // Add custom user claims here
    
            return userIdentity;
        }
     ...all the other things
    }
    

    I then created my own Identity Manager (interface)

    public interface IIdentityManager
    {
        //User manager methods
        Task<IIdentityResult> CreateAsync(IApplicationUser user);
        //..all methods needed
    }
    
    public interface IIdentityResult
    {
        bool Succeeded { get; set; }
        List<string> Errors { get; set; }
    }
    

    Then my actual implementations of this, so this one is for my .NET Core projects.

    public class IdentityManagerCore : IIdentityManager
    {
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly RoleManager<ApplicationRole> _roleManager;
    
        public IdentityManagerCore(UserManager<ApplicationUser> userManager, RoleManager<ApplicationRole> roleManager)
        {
            _userManager = userManager;
            _roleManager = roleManager;
        }
    
        public async Task<IIdentityResult> CreateAsync(IApplicationUser user)
        {
            ApplicationUser realUser = new ApplicationUser()
            {
                Id = user.Id,
                TemporaryToken = user.TemporaryToken,
                AccessFailedCount = user.AccessFailedCount,
                ConcurrencyStamp = user.ConcurrencyStamp,
                Email = user.Email,
                EmailConfirmed = user.EmailConfirmed,
                LockoutEnabled = user.LockoutEnabled,
                LockoutEnd = user.LockoutEnd,
                NormalizedEmail = user.NormalizedEmail,
                NormalizedUserName = user.NormalizedUserName,
                PasswordHash = user.PasswordHash,
                PhoneNumber = user.PhoneNumber,
                PhoneNumberConfirmed = user.PhoneNumberConfirmed,
                RequiresPasswordCreation = user.RequiresPasswordCreation,
                SecurityStamp = user.SecurityStamp,
                TwoFactorEnabled = user.TwoFactorEnabled,
                UserName = user.UserName
            };
            var result = await _userManager.CreateAsync(realUser);
            return ConvertToInterface(result);
        }
    
        private IIdentityResult ConvertToInterface(IdentityResult result)
        {
            IIdentityResult realResult = new IdentityResultCore();
            realResult.Succeeded = result.Succeeded;
            realResult.Errors = result.Errors?.Select(x => x.Description).ToList();
            return realResult;
        }
    }
    
    public class IdentityResultCore : IdentityResult, IIdentityResult
    {
           private IEnumerable<string> _errors;
        private bool _succeed;
        public new bool Succeeded
        {
            get => base.Succeeded || _succeed;
            set => _succeed = value;
        }
    
        public new List<string> Errors
        {
            get => base.Errors?.Select(x => x.Description).ToList() ?? _errors?.ToList();
            set => _errors = value;
        }
    }
    

    The UserManager and RoleManager are injected on startup like so:

    services.AddTransient<IIdentityManager, IdentityManagerCore>();
    

    The .NET Framework implementation:

    public class IdentityManagerMvc : IIdentityManager
    {
        private readonly UserManager<ApplicationUserMvc, string> _userManager = null;
        private readonly RoleManager<ApplicationRoleMvc, string> _roleManager = null;
    
        internal static IDataProtectionProvider DataProtectionProvider { get; private set; }
        public IdentityManagerMvc(DatabaseContextMvc dbContext)
        {
            var userStore =
                new UserStore<ApplicationUserMvc, ApplicationRoleMvc, string, ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim>(dbContext);
            var roleStore = new RoleStore<ApplicationRoleMvc, string, ApplicationUserRole>(dbContext);
            _userManager = new UserManager<ApplicationUserMvc, string>(userStore);
            _roleManager = new RoleManager<ApplicationRoleMvc, string>(roleStore);
            _userManager.UserValidator = new UserValidator<ApplicationUserMvc>(_userManager) { AllowOnlyAlphanumericUserNames = false };
            if (DataProtectionProvider == null)
            {
                DataProtectionProvider = new MachineKeyProtectionProvider();
            }
            _userManager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUserMvc, string>(DataProtectionProvider.Create("Identity"));
        }
    
        public async Task<IIdentityResult> CreateAsync(IApplicationUser user)
        {
            ApplicationUserMvc realUser = new ApplicationUserMvc()
            {
                Id = user.Id,
                TemporaryToken = user.TemporaryToken,
                AccessFailedCount = user.AccessFailedCount,
                ConcurrencyStamp = user.ConcurrencyStamp,
                Email = user.Email,
                EmailConfirmed = user.EmailConfirmed,
                LockoutEnabled = user.LockoutEnabled,
                LockoutEnd = user.LockoutEnd,
                NormalizedEmail = user.NormalizedEmail,
                NormalizedUserName = user.NormalizedUserName,
                PasswordHash = user.PasswordHash,
                PhoneNumber = user.PhoneNumber,
                PhoneNumberConfirmed = user.PhoneNumberConfirmed,
                RequiresPasswordCreation = user.RequiresPasswordCreation,
                SecurityStamp = user.SecurityStamp,
                TwoFactorEnabled = user.TwoFactorEnabled,
                UserName = user.UserName
            };
            var result = await _userManager.CreateAsync(realUser);
            return ConvertToInterface(result);
        }
    
        private IIdentityResult ConvertToInterface(IdentityResult result)
        {
            IIdentityResult realResult = new IdentityResultMvc();
            realResult.Succeeded = result.Succeeded;
            realResult.Errors = result.Errors?.ToList();
            return realResult;
        }
    }
    
    
    public class IdentityResultMvc : IdentityResult, IIdentityResult
    {
        private IEnumerable<string> _errors;
        private bool _succeed;
        public new bool Succeeded
        {
            get => base.Succeeded || _succeed;
            set => _succeed = value;
        }
    
        public new List<string> Errors
        {
            get => base.Errors?.ToList() ?? _errors?.ToList();
            set => _errors = value;
        }
    }
    

    Last but not least you would need a seperate DatabaseContext for your .NET Framework project, this one will only be used for "identity" purposes, so not to actually query any data, just for authentication, authorization.

    public class DatabaseContextMvc : IdentityDbContext<ApplicationUserMvc, ApplicationRoleMvc, string, ApplicationUserLogin,
        ApplicationUserRole, ApplicationUserClaim>
    {
        public DatabaseContextMvc() : base("DatabaseContext")
        {
            Configuration.LazyLoadingEnabled = false;
            Configuration.ProxyCreationEnabled = false;
    
            //Database.SetInitializer<DatabaseContextMvc>(null);
        }
    
        public void SetTimeout(int minutes)
        {
            this.Database.CommandTimeout = minutes * 60;
        }
    
        public static DatabaseContextMvc Create()
        {
            return new DatabaseContextMvc();
        }
    }
    

    At this moment you should have all the classes necessary to use it everywhere. So for example, in your .NET Framework project, you can have your ApplicationUserManager like so:

    public class ApplicationUserManager : UserManager<ApplicationUserMvc, string>
    {
        public ApplicationUserManager(IUserStore<ApplicationUserMvc, string> store)
            : base(store)
        {//look at where i used applicationUserMvc
        }
    
        public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) 
        {
            var userStore =
                new UserStore<ApplicationUserMvc, ApplicationRoleMvc, string, ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim>(context.Get<DatabaseContextMvc>());
            //ApplicationUserLogin,UserRole,UserClaim are self created but just override IdentityUserLogin (for example).
            var manager = new ApplicationUserManager(userStore);
        }
      ...
    }
    

    In whatever Dependency Injection you use in .NET Framework, make sure to Register both your DatabaseContext and DatabaseContextMvc.

    Here is my DatabaseContext which is inside the .NET Standard library and used across .NET Core and .NET Framework:

    public class DatabaseContext : IdentityDbContext<ApplicationUser, ApplicationRole, string, ApplicationUserClaim, ApplicationUserRole, ApplicationUserLogin, ApplicationRoleClaim, ApplicationUserToken>
    {
        private IConfiguration _config;
        public string ConnectionString { get; }
        public DatabaseContext(IConfiguration config) : base()
        {
            _config = config;
            var connectionString = config.GetConnectionString("DatabaseContext");
            ConnectionString = connectionString;
        }
    
        public void SetTimeout(int minutes)
        {
            Database.SetCommandTimeout(minutes * 60);
        }
    
        public virtual DbSet<Address> Addresses { get; set; }
    
        public static DatabaseContext Create(IConfiguration config)
        {
            return new DatabaseContext(config);
        }
    
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            //setup code for the DbContextOptions
            optionsBuilder
                .UseSqlServer(ConnectionString, 
                    providerOptions => providerOptions.CommandTimeout(60))
                .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
            base.OnConfiguring(optionsBuilder);
        }
    

    your focus is probably already gone after this long post or after implementing, but A few more words:

    • I can guarantee that with this you will make it work, it works perfect for the last half year.
    • So .NET Standard (and thus EntityFrameworkCore) is the primary way of doing database operations, most code is to cope with .NET Framework.
    • You will fight with installing the correct dependencies. It will cause runtime exceptions that you will encounter very quickly but easy resolvable, .NET Framework just needs to install the dependencies for itself. Make sure that the versions are aligned, use the Consolidate version under Manage NuGet packages for Solution (right-click on solution).
    • You will have to do it the-netcore-way for settings: so you need appsettings.json in your .NET Framework project as well. Also you still need it in the Web.Config, this is mostly for the small part of authentication.

      private IConfiguration GetConfiguartion()
      {
          var path = Server.MapPath("~/");
          var builder = new ConfigurationBuilder()
                           .SetBasePath(path)
                           .AddJsonFile("appsettings.json");
      
          return builder.Build();//inject the return IConfiguration in your DI
      }
      
    • Good luck. If you think this is difficult and causes a lot of trouble: correct, if you have a small application you are better of converting everything to .NET Core / .NET Standard.