Saturday, April 9, 2016

Managing identity when migrating from legacy to MVC - Part 2

The first place to start is with your IdentityModel file.  Depending on how your legacy application is set up to do authentication you may have to override more or less than I have.  Starting with the ApplicationUser, which I overrode as follows:

public class ApplicationUser : IdentityUser<string, IdentityUserLogin, IdentityUserRole, ApplicationUserClaim> {...}

Here is the default definition of ApplicationUser for comparison:

public class ApplicationUser : IdentityUser {...}
public class IdentityUser : IdentityUser<string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim>, IUser, IUser<string>

I had to override the definition of IdentityUser because I added a property to IdentityUserClaim.

public class ApplicationUserClaim : IdentityUserClaim<string>
    {
        public string Issuer { get; set; }
    }

public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string, ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim>
{
   public ApplicationDbContext(string connectionNameOrString) : base(connectionNameOrString)
   {
//Keep EF from trying to track this
Database.SetInitializer<ApplicationDbContext>(null);
        }
        public ApplicationDbContext() : this("SecurityContext"){}

public IDbSet<ApplicationUserClaim> Claims { get; set; }

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new ApplicationUserConfiguration());  modelBuilder.Configurations.Add(new IdentityRoleConfiguration());  modelBuilder.Configurations.Add(new IdentityUserRoleConfiguration());  modelBuilder.Configurations.Add(new IdentityUserLoginConfiguration());  modelBuilder.Configurations.Add(new ApplicationUserClaimConfiguration());  }

public static ApplicationDbContext Create()
{
return new ApplicationDbContext("SecurityContext");

}

    }

I use the issuer property in ApplicationUserClaim for storing the clientId that claims belong to, as some users can belong to multiple clients, with different permissions in each.

ApplicationDbContext is where you will do your Entity Framework mappings to your legacy tables.  I created a number of views to abstract this away from the base tables that the application used, mainly to deal with translating page level permissions in the application into claims.  Here is the EntityTypeConfiguration for ApplicationUserClaim, the others are done similarly.

public class ApplicationUserClaimConfiguration : EntityTypeConfiguration<ApplicationUserClaim>
{
        /// <summary>
        /// Claims returned from this query will be stored in the application auth cookie
        /// </summary>
public ApplicationUserClaimConfiguration()
{
ToTable("vwUserClaims");
Property(c => c.UserId).HasColumnName("entityContactId");
}
}


For your claims view, you will by default need to return a Claim Type, Claim Value, and User Id (the framework selects claims based on the user Id).  Because I defined the Issuer property on my ApplicationUserClaim I also had to return an Issuer column.  Make note that depending on your implementation the claims that are returned for the user from this view can get stored in the application cookie.  I made the mistake of sending all claims for sysadmin users instead of doing a check for sysadmin on the server, causing my cookie size to exceed 4k and be split, which also causes lots of unnecessary traffic to the server for sysadmin users.

Application claims are not by default stored in the application cookie, and are not even the same class as claims that are stored in the application cookie, so if you want to store your application claims in the application cookie you will need to translate between your application claims class and the System.Security.Claims.Claim class.  This is done in the ApplicationUserStore in the GetClaimsAsync method, shown below.

public class ApplicationUserStore : UserStore<ApplicationUser, IdentityRole, string, IdentityUserLogin, IdentityUserRole, ApplicationUserClaim>
{
private ApplicationDbContext myContext;
   private ILog log = LogManager.GetLogger(typeof (ApplicationUserStore));

public ApplicationUserStore(ApplicationDbContext context) : base(context)
{
myContext = context;
}
        /// <summary>
        /// this is where we could translate any custom properties on the <see cref="ApplicationUserClaim"/> to something on the <see cref="Claim"/>
        /// </summary>
        /// <param name="user"></param>
        /// <returns></returns>
public override Task<IList<Claim>> GetClaimsAsync(ApplicationUser user)
{
            log.Debug("Getting Claims");
var appClaims = myContext.Claims.Where(c => c.UserId == user.Id).ToList();
            //be careful turning this on, there can be a lot of claims and logging them all can impact performance
            appClaims.ForEach(c=>log.Verbose(c.ToString()));
return Task.FromResult<IList<Claim>>(appClaims.Where(c=>c.ClaimValue!=null).Select(c => new Claim(c.ClaimType, c.ClaimValue, null, c.Issuer)).ToList());
}

   public override async Task UpdateAsync(ApplicationUser user)
   {
            //We don't allow any updates except to password
            log.DebugFormat("trying to update user {0}", user.Id);
       //await base.UpdateAsync(user);
   }

   public override Task SetPasswordHashAsync(ApplicationUser user, string passwordHash)
   {
       return base.SetPasswordHashAsync(user, passwordHash);
   }
}