Keep In Touch

Migrating from ASP.NET Membership to ASP.NET Identity

Update: I'm humbled and excited that this post was chosen as Article of the Day on the offical Microsoft ASP.NET website for March 31, 2015. You used to be able to see it posted here but there's no permalink option. Sorry!

The Story

One of my websites (a Web Forms application founded in 2007) was created using the ASP.NET Membership provider. For many years, the Membership framework served me well and the site hasn’t changed substantially so major infrastructure-level changes were never necessary. Fast forward to present day. This legacy application is undergoing some major changes and growth, and the logical step to support it in the future is to move away from the ASP.NET Membership provider and toward a newer, more extensible framework.

Enter ASP.NET Identity, a subset of Windows Identity Foundation.

Before I dig in too much, I want to make sure I give credit where credit is due. The following articles helped me through this process:

ASP.NET Identity – User Lockout - Helped clarify the way a user is locked out in ASP.NET Identity vs. ASP.NET Membership. This also discusses the use of SignInManager which I did not end up using (explained later).

My Scenario

In this incredibly short post I’m going to outline the steps that I took to migrate my code and users to the new ASP.NET Identity framework. I’ll provide code samples and some gotchas that I ran into. But first, here’s a summary of my situation, since yours may differ:

The application uses Web Forms and ASP.NET Membership. At the time of this migration, there was no use of MVC. If you do use MVC though, the code is still applicable (I think!)

Entity Framework (v.6) was introduced fairly recently in a database-first approach. This is not largely relevant, but I do use Entity Framework in some optional steps below.

Roles and Profiles were not used in this site, so I won’t be covering them.

I discarded a lot of the Membership data from the [aspnet_Membership] table; I just really didn’t need most of it.

As much as possible, I wanted to preserve the vanilla implementation of IdentityUser, the model that ASP.NET Identity uses, so any of the additional data from the old Membership tables that I did keep is stored in a separate table.

ASP.NET Identity does not, by default, utilize a security question and answer for password reset. I wanted to preserve this functionality since my site uses it, so I will outline how I did that.

My application database used a single Application ID from the Membership database, so I did not cover the case of a multi-tenant Membership database.

My application is split into a few projects, so if you’re not sure where to put the code I provide, just put it somewhere. Refactor and move it once you understand it (which is exactly what I did.)

Many examples online use Database Migrations to create an extended version of the basic Identity models, but again for the purpose of preserving as much about the vanilla integration of ASP.NET Identity I did not use this approach; if you want to, the process is documented in the first article I linked above.

The Fundamentals

ASP.NET Identity uses a class called IdentityUser which it maps to a database table called [AspNetUsers] to persist user data. I’m not sure if the naming of this table is merely convention, but it’s the default and it seems to work. In combination with this, we also have a UserManager which manages the persistence of the user model. This class provides functionality like UpdateUser(...), ChangePassword(...), GetRoles(...), etc. It replaces much of the functionality from the legacy MembershipUser and rolls them into a manager-type class.

We will be extending IdentityUser into our own subclass and calling it ApplicationUser (pretty common, actually), and likewise extending the UserManager into our own subclass called ApplicationUserManager. This does a few things: first, it allows us to extend the default UserIdentity class if we wanted to (we won’t be), and secondly it allows us to override some properties of the default UserManager; the most important property we’ll need to override is the PasswordHasher with our own implementation. Since ASP.NET Identity’s default UserManager has a different hashing algorithm (PBKDF2) than the legacy one used in ASP.NET Membership (SHA-1 by default), we’ll provide our own implementation of it called SqlPasswordHasher, which is capable of validating both types. This allows us to migrate our users as-is and still allow them to log in.

We’ll also need to create a new implementation of IdentityDbContext, which we’ll call ApplicationDbContext. This just gives Entity Framework a context through which to persist the user data. Don’t worry too much about the details of this, because it’s one line of code.

[AspNetUsersExt] - This is a custom table, and I use it to store additional details from the Membership tables which allows me to keep the [AspNetUsers] table clean and simple. In this example, I will be storing the security question/answer credentials (and only these). You can change the name if you’d like.

[AspNetRoles] - Roles live here (Won’t be covered in this post)

[AspNetUserRoles] - Maps users to roles (Won’t be covered in this post)

[AspNetUserClaims] - Maps users to claims (if you’re not familiar, a claim is a simple but verifiable statement about a user that you define, e.g. “CanDeletePosts” or “PassportNumber”). Claims work alongside roles, but provide more granular assertions about what a user is authorized to do. (Won’t be covered in this post)

[AspNetUserLogins] - This table stores information about other logins that a user has verified, like Facebook or Twitter (Won’t be covered in this post)

Let’s Get Started

Create The ASP.NET Identity Tables

Update Existing Connection String To Add providerName

Add References To ASP.NET Identity Assemblies

Add Code For ASP.NET Identity Functionality

Get Login/Signup Working

Migrate Membership Database Users

Step 1. Create The ASP.NET Identity Tables

The first step I recommend is to create the database tables. Since this just means adding a few new tables, the chance of breaking anything is relatively low. The following script will create the tables described above (sorry, it’s long, but it works on SQL Azure.) I’m not sure why datetime is used instead of datetime2 but it’s working in my project. Feel free to separate these out into individual scripts:

Now that we have our tables, we can add the requisite assemblies and classes to our solution which give us the ASP.NET Identity functionality:

Step 2. Update Existing Connection String To Add providerName

If your connection string is missing a providerName attribute, make sure you add it. This is required for the persistence mechanism in ASP.NET Identity to work.

Gotcha Warning: This one really threw me. Long story short, I forgot to update this in my release Web.config transform and took down my production site (only for a few seconds, fortunately!) So, if you use config transforms, make sure you check this!

Step 4. Add Code For ASP.NET Identity Functionality

Now that we have our database tables and all of our required assemblies, we need to add the code which ties it all together. If you’re using a single project, you can put these wherever you want. If your application is split into several projects like mine, put them in a deeper assembly like your data tier or authentication layer.

c. Add The SqlPasswordHasher Class

This class will be responsible for authenticating old Membership and new Identity passwords. It uses a specific convention which I will explain later, but all you need to know now is that the old Membership password will be stored in a pipe-delimited format in our database after we migrate our users.

// SqlPasswordHasher.cspublicclassSqlPasswordHasher:PasswordHasher{publicoverridePasswordVerificationResultVerifyHashedPassword(stringhashedPassword,stringprovidedPassword){string[]passwordProperties=hashedPassword.Split('|');if(passwordProperties.Length!=3){returnbase.VerifyHashedPassword(hashedPassword,providedPassword);}else{stringpasswordHash=passwordProperties[0];intpasswordformat=1;stringsalt=passwordProperties[2];if(String.Equals(EncryptPassword(providedPassword,passwordformat,salt),passwordHash,StringComparison.CurrentCultureIgnoreCase)){returnPasswordVerificationResult.SuccessRehashNeeded;}else{returnPasswordVerificationResult.Failed;}}}// This is copied from the existing SQL providers and is provided only for back-compat.privatestringEncryptPassword(stringpass,intpasswordFormat,stringsalt){if(passwordFormat==0)// MembershipPasswordFormat.Clearreturnpass;byte[]bIn=Encoding.Unicode.GetBytes(pass);byte[]bSalt=Convert.FromBase64String(salt);byte[]bRet=null;if(passwordFormat==1){// MembershipPasswordFormat.Hashed HashAlgorithmhm=HashAlgorithm.Create("SHA1");if(hmisKeyedHashAlgorithm){KeyedHashAlgorithmkha=(KeyedHashAlgorithm)hm;if(kha.Key.Length==bSalt.Length){kha.Key=bSalt;}elseif(kha.Key.Length<bSalt.Length){byte[]bKey=newbyte[kha.Key.Length];Buffer.BlockCopy(bSalt,0,bKey,0,bKey.Length);kha.Key=bKey;}else{byte[]bKey=newbyte[kha.Key.Length];for(intiter=0;iter<bKey.Length;){intlen=Math.Min(bSalt.Length,bKey.Length-iter);Buffer.BlockCopy(bSalt,0,bKey,iter,len);iter+=len;}kha.Key=bKey;}bRet=kha.ComputeHash(bIn);}else{byte[]bAll=newbyte[bSalt.Length+bIn.Length];Buffer.BlockCopy(bSalt,0,bAll,0,bSalt.Length);Buffer.BlockCopy(bIn,0,bAll,bSalt.Length,bIn.Length);bRet=hm.ComputeHash(bAll);}}returnConvert.ToBase64String(bRet);}}

Step 5. Get Login/Signup Working

Your implementation of login and signup may differ, but here are a few key points:

User creation is done by populating an instance of ApplicationUser and then passing it to an instance of ApplicationUserManager to persist.

The ApplicationUserManager provides a public property for PasswordHasher. We can directly reference this to utilize our SqlPasswordHasher and log users in whether they provide their legacy ASP.NET Membership password, or a new ASP.NET Identity password. This is why I do not use an instance of a SignInManager, which otherwise doesn’t provide the lower-level interfaces to do this (that I’m aware of–also this works so I didn’t want to change it.)

If a user logs in with their old Membership password, we will take advantage of having their password in memory and reset the user’s password to the new format. This isn’t necessary, but it’s a nice option to have.

In the user migration scripts below, notice how we concatenate our old Membership password with the password format and salt? The SqlPasswordHasher has a method called VerifyHashedPassword(...) that returns one of three results:

PasswordVerificationResult.Success - All good, the user’s password is valid

PasswordVerificationResult.Failed - The user’s password could not be validated

PasswordVerificationResult.SuccessRehashNeeded - The user’s password was validated by our SqlPasswordHasher but we have determined that it was a legacy Membership password. At this point we may choose to re-apply the user’s password.

a. Create A User

Here, we create a user and then add their security question and answer to the data store. Note that after you create the account, you would have to log the user in as well. The details of how to do that are covered next.

varuser=newApplicationUser();user.UserName=username;user.Email=email;user.EmailConfirmed=true;user.LockoutEnabled=true;if(_userManager.Create(user,password)==IdentityResult.Success){// Add the user's security question and answer// This uses another Entity Context, and is separate from ASP.NET IdentityvaruserExts=newAspNetUsersExt();userExts.UserId=user.Id;// These 3 properties are up to you; my implementation may be different// than what you needuserExts.SecurityQuestion=securityQuestion;userExts.SecurityAnswerSalt=salt;userExts.SecurityAnswer=securityAnswer;using(varentities=newMyEntities()){entities.AspNetUsersExts.Add(userExts);entities.SaveChanges();}// All done// You would now log the user in}

b. Log In With The User

Below is quick example of how you could log in a user, with an example of how to re-hash the password. This example uses OWIN context, which admittedly I don’t really understand at this point, so just copy/paste that part. This is a refactored version of my code, but should give you a pretty good idea:

protectedboolLogin(stringusername,stringpassword){// Instantiate a new ApplicationUserManager and find a user based on provided UsernamevaruserManager=newApplicationUserManager();varuser=userManager.FindByName(username);// Invalid user, fail loginif(user==null||userManager.IsLockedOut(user.Id)){InvalidLogin();// Do something here to tell the userreturnfalse;}// Valid user, verify passwordvarresult=userManager.PasswordHasher.VerifyHashedPassword(user.PasswordHash,password);if(result==PasswordVerificationResult.Success){UserAuthenticated(userManager,user);}elseif(result==PasswordVerificationResult.SuccessRehashNeeded){// Logged in using old Membership credentials - update hashed password in database// Since we update the user on login anyway, we'll just set the new hash// Optionally could set password via the ApplicationUserManager by using// RemovePassword() and AddPassword()user.PasswordHash=userManager.PasswordHasher.HashPassword(password);UserAuthenticated(userManager,user);}else{// Failed login, increment failed login counter// Lockout for 15 minutes if more than 10 failed attemptsuser.AccessFailedCount++;if(user.AccessFailedCount>=10)user.LockoutEndDateUtc=DateTime.UtcNow.AddMinutes(15);userManager.Update(user);InvalidLogin();// Do something here to tell the userreturnfalse;}}privatevoidUserAuthenticated(ApplicationUserManageruserManager,ApplicationUseruser){// Create an instance of an AuthenticationManager and Identity to authenticate and sign in the user// If all goes well, redirect the user to either the querystring's return URL, or their accountvarauthenticationManager=HttpContext.Current.GetOwinContext().Authentication;varuserIdentity=userManager.CreateIdentity(user,DefaultAuthenticationTypes.ApplicationCookie);authenticationManager.SignIn(newAuthenticationProperties(){IsPersistent=false},userIdentity);user.AccessFailedCount=0;user.LockoutEndDateUtc=null;userManager.Update(user);Response.Redirect(Request.QueryString["ReturnUrl"]??"~/Account/");}

c. Log The User Out

Fairly straightforward. This kills the auth cookie and logs the user out:

Step 6. Migrate Membership Database Users

And now the fun part. Do you have good database backups? Yes? Okay good. Back them up again.

In all truth, this script is pretty simple and should be idempotent, but as always, if you’re running queries on a production database, make sure you have a reliable backup. Is your backup script done yet? Okay good let’s move on.

This last step consists of running a script to find users in the [aspnet_Membership] table, joined with [aspnet_Users], and copy them over into the new [AspNetUsers] and, optionally, [AspNetUsersExt] tables. Remember again, the only thing we’re copying into the extended table are the security question and answer fields. If you’re not using the security question and answer method of password reset in your Membership implementation, you can ignore this table (or use it as an example for migrating any other data you may want to keep.)

In the script below, you can alter the TOP 1 to any number that you are comfortable with. I started with 1, and bumped it up to 1,000, then 5,000, then 10,000. In my SQL Azure database, it took roughly 20-25 seconds to migrate 10,000 users. I ran that batch a few times until all of my users were migrated, about 75,000 in total.

The script will copy the basic user details, but with a few things to note:

The password will be copied as a 3-part delimited string. Again, our SqlPasswordHasher will make use of these pipe delimiters.

Since ASP.NET Identity uses a combination of two fields to determine if a user is locked out ([LockoutEnabled] determines that the user can be locked out, and [LockoutEndDateUtc] actually does the work) we need to set both fields if a user is locked out in the Membership database. In my website, I unfortunately deal with a lot of fake accounts and spammers, so the technique I use is to set [LockoutEnabled] to 1 for all users, and to set [LockoutEndDateUtc] to an arbitrary 1,000 years in the future for locked out Membership accounts. If my site is still up in 1,000 years, they can have their accounts back.

The whole thing happens in a transaction. Smart, right?

The first part of the query migrates a batch of users idempotently. The second part migrates any security question/answers that were not already moved.

Note that we copied the Security Answer Salt from the Membership table. That’s because the PBKDF2 passwords in ASP.NET Identity have a built-in salt, so a separate field is no longer needed in the database to store it. However, we still need it for our security question/answer. I haven’t yet refactored this, but by this point I’m sure you can imagine how.

Gotcha Warning: You’ll notice that the new users table has a field called [SecurityStamp]. This is not a password salt so please do not treat it as such. This is a token used to verify the state of an account and is subject to change at any time. Do not use it as a salt for your application, or for your security question/answer.

All Done… Almost!

At this point we’ve completed all the requisite steps to move users and make them available to log in. You should be able to log in using any newly-created ASP.NET Identity user, or any legacy migrated Membership user.

Gotcha Warning: One thing I wanted to point out is that the [AspNetUsers] table uses a Guid ID. This table likewise does not have a signup/create date for users. The end result is a table of users with no discernable order nor signup date. I personally prefer to have a signup date and order for users, so I later added a column to the table called [CreatedOnUtc]. You can also use an auto-incrementing integer, but it takes some code and database changes which you’ll need to research.