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!
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:
- Migrating an Existing Website from SQL Membership to ASP.NET Identity - Much of the code I provide below comes from this article. The information I provide below is similar, but I tried to clarify some areas that confused me about that article.
- Setting Up ASP.NET Identity Framework 2.0 with Database First - Because I was using a database-first approach in my solution, this helped clarify some details for me from the above article.
- 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).
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.
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.
So in the end we will end up with 4 new classes:
- ApplicationUser - Model which represents a user
- ApplicationUserManager - Manager class which provides all functionality for adding/updating/querying users
- ApplicationDbContext - Context class that Entity Framework uses to persist a user model to the data store
- SqlPasswordHasher - A magic class that can validate new PBKDF2 passwords, and likewise old SHA-1 passwords for migrated accounts
We’ll also end up with 6 new tables in our database, which I’ll provide the SQL queries to create. I won’t be covering most of these, but you should still create and know about them for the future:
- [AspNetUsers] - Stores our users, and maps to the ApplicationUser model above
- [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)
- 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
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 3. Add References To ASP.NET Identity Assemblies
You will need to add references to the ASP.NET Identity assemblies, preferably using NuGet as below:
Optionally if you later want to include Facbeook, Twitter, or other authentication providers, you can do so now (optional):
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.
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.
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.
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.
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:
Fairly straightforward. This kills the auth cookie and logs the user out:
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.
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.
To add the column to [AspNetUsers] with a default UTC date and time:
To update the values from the Membership database:
The [CreatedOnUtc] field does not necessarily need to be added to your ApplicationUser model, though you could add it. I simply wanted to have the data in case I need it in the future.
Please let me know if you have any questions, comments, or concerns below.
If you have a project that needs help, a process that needs improvement, or an idea that you want a sounding board for, I would love to have a discussion with you.
I'm currently employed as a Senior Consultant at Magenic and not looking for other opportunities at this time.
Want free advice, thoughts, or feedback? Tell me what you're working on or describe a challenge you're facing and I'll do my best to help.
Be awesome. Download my free 56-page eBook for building performant, scalable, maintainable software using .NET Web API. (There's also a bonus chapter on effectively using HTTP Status Codes.)
Enter your email address below and get it immediately.
ASP.NET Identity 3 without Roles and using only Claims #581
Is it possible to use ASP.NET Identity 3 in a MVC project only with Claims table and without Roles table?
I am asking this because Role is itself a Claim of type Role so isn't it redundant to have a Roles table?
mdmoura changed the title from ASP.NET Identity 3 without Roles and using only Claimns to ASP.NET Identity 3 without Roles and using only Claims Oct 5, 2015
In a ASP.net Identity on ASP.net MVC 5 is fully possible. In MVC 6 I guess, yes!
1 - Using only roles and having one roles table;
In this case there would be the RolesStore .
2 - Using claims, including roles, and having only one claims table.
In this case there would be the ClaimsStore . Role is a Claim of type Role.
If this results in unwanted complexity why not having only one Claims table and if configuration includes only Roles then Claims table would allow only claims of type Role .
I just think that having Roles and Claims tables when a Role is a Claim does not make sense.
Do you know where can I find an example of how to set ASP.NET Identity only with Claims?
I was looking and didn't find it . I just found info on how to add claims on top of roles.
Better documentation for ASP.NET Identity would be great .
And it would even help people to give better feedback .
53identityalert.com 7 years old and has com extension. TLD may be using diffrent purposes. Example: .com extension inherited from "commercial" and uses for generally commercial activities, .net extension inherited from "network" and uses for generally internet activites etc. Its SLD length is equal to 15. SLD length is important for human-readable and brand-friendly and seo-friendly. It must be less than 10 and meaningful that you are/will working on activites.
53identityalert.com registered by CSC CORPORATE DOMAINS, INC. for Domain Admin on 10-10-2008. Domain Admin renewed on 10-06-2015 until 10-10-2016.
53identityalert.com serves on -, United States and establish connection to the internet via Cuc International Internet Service Provider (ISP)
A record assigned to 22.214.171.124.
The bellow graphic illustrates resource sizes the home page of 53identityalert.com. As you can see, Html size is 25.0KB and Text size is 1.7KB. This ratios are important for SEO. Text ratio grather than Html Ratio indicates a quality content to Search Engines and a ranking signal.
User behaviors change everyday. And users aren't patience on the internet. At this point page speed gaining importance. Your pages loading time must be less than 3-5 seconds and response time must be less than or equals 200ms.