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);
   }
}

Monday, March 28, 2016

Managing identity when migrating from legacy to MVC

Recently I've been working on migrating a set of sites from classic ASP and Webforms sites to Asp.Net MVC, and part of that has been to "integrate" the logins.  I'm going to document how I did this and the places you'll need to plug into MVC Identity to get it to work with existing systems you don't want to change yet.

This legacy application had its own pre-existing login system, not any standard forms auth table structure or anything else, so I'll show you how to plug into that with Identity.

Issue 1 - cookies

The pre existing site was already using custom cookies to manage login info, so I had to co-ordinate between the pre-existing cookies and cookies from Webforms forms authentication.  Most important, make sure the authentication forms element is using the same name attribute between any sites where you want to share, and that the machineKey is also the same across web sites/applications where you want to share login (otherwise one site won't be able to decrypt the other's cookie).

Issue 2 - domains

This can really cause a headache.  If you're trying to share between to b-level domains (i.e. abc.com and xyz.com) you're going to probably need some other way of doing SSO, as the cookies aren't going to be shared between these domains by the browser.  My sites were all c level (i.e. site1.abc.com, site2.abc.com, etc) so I could set the cookies at the b-level domain and thus share across all the sub sites.  I'll go into details on how to up the setting of the cookies to be a the b level domain instead of their normal setting at the c level one.  There is actually an easier way to do this than the way I had to, and I'll describe that as well.

Those were the biggest issues, the rest is just the process of hooking into ASP.Net Identity in the (I hope, as I couldn't find any authoritative documentation out there on the "right" way to do some of this) correct places.