When you work on an old code base, it’s boring. You want to upgrade to the newest and latest. However, it is not always easy to follow the latest trend without suffering inevitable breaking changes.
Recently, I wanted to update an old MVC4 application to MVC5 in order to use the power of Owin. If you have a small website, then I would recommend you follow this tutorial from Rick Anderson and Suhas Joshi: http://www.asp.net/identity/overview/migrations/migrating-an-existing-website-from-sql-membership-to-aspnet-identity
It explains how to migrate an SQL Membership to ASP.Net Identity by upgrading your database. Not only does it sound scary if you have thousands of users, but your website will go through some downtime during the migration (hopefully only a short period if everything goes according to plan). I decided to take a different approach: use the Owin extensibility to plugin the existing database; no schema changes!
I have submitted the code on GitHub if you want to take a look—keep reading to help your upgrade process!
Download All NuGet Packages
Sounds trivial—the other tutorial also tells you to do that! I will not install all the packages. I will focus on migrating to something stable before adding any new functionalities (Google, Facebook, Twitter OAuth, etc.).
If you install these two packages, the dependencies will follow:
- Microsoft.AspNet.WebApi.Owin (If you are upgrading WebAPI)
Bring On the New Code!
Owin is using mainly three components: “SignInManager”, “UserManager”, and a “UserStore”. We will create our own here:
Your database context
My app was using the Legacy ObjectContext code generation—not the easiest one to work with. I have updated to T4 templates. Once you have done that, you should have a generated class called “
aspnet_Membership“. There are two things you have to do make this file work with Owin.
- Implement that new interface
Id should point to
UserName should point to
As Nicholas pointed out in the comments, you can create another partial file within the same namespace instead of editing the auto-generated file. That way, you don’t have to modify any T4 templates when re-generating your C# code. You can find more information about partials on the MSDN C# Programming Guide.
This file doesn’t have much to it, but you need to extend
SignInManager with your context like so:
The first generic is your context, the second generic is the
UserId key type.
Not much difference in this file with the exception that the passwords in the old database are not hashed the same way that Owin usually hashes them. By following the tutorial from Rick, I have created a password hasher (SQLMembershipPasswordHasher) similar to what he proposed, but I have added a password format.
The format is written in the database and follows the format from MembershipPasswordFormat. I have added the Owin format to the integer 4.
When a user changes his password, or when we create a new user, we will have its hash set to 4.
Interestingly, the “
SuccessRehashNeeded” enum actually doesn’t do anything. It won’t rehash your password.
This file is the biggest beast of all! By implementing multiple interfaces used by the
UserManager, we are able to close the gap with our database schema. There are a couple gotchas to notice.
The Infamous ApplicationName
Hopefully your site supports only one application, because I have not added support for more than that. In the Create static method, the second parameter indicates the name of your application name. Here it is called “/” (yep, a slash)
In your membership provider from the Web.config, you should be able to find plenty of settings. Don’t forget to port them.
The Lockout System
Instead of spending countless hours on this, I decided to make a different behavior than the one that exists in Membership. I will explain how both work.
Membership has 3 fields handling the lockout:
- IsLockedOut: this indicates the user is locked out. There is no way to unlock the user unless you turn off this bit by setting it to 0.
- FailedPasswordAttemptCount: counts the number of times the password has been entered incorrectly during a period of time decided by “passwordAttemptWindow”.
- FailedPasswordAttemptWindowStart: indicates when the first incorrect password has been entered. After
maxInvalidPasswordAttemptsis reached within the
passwordAttemptWindowtime limit, the IsLockedOut bit goes to 1.
Asp.Net Identity has 3 fields handling the lockout:
- LockoutEnabled: if this bit it set to 0, your user will NEVER be allowed to be locked out. This could be useful for admins.
- AccessFailedCount: pretty trivial name.
- LockoutEndDateUtc: indicates when the user will be allowed to sign in again. Which means that if this date is in the future, the user is locked out.
As you can see, there are two different applications. If I want to keep the exact same behavior, I’m going to have some trouble. I decided to embrace the Asp.Net Identity idea and lock out a user only for a period of time. For this, I have repurposed the field “
FailedPasswordAttemptWindowStart” to act as “
FailedPasswordAttemptCount used to work based on a time period window, I have changed it to simply be a counter like
To verify if a user is locked out, the
UserManager asks what the lock out end date is. If the
IsLockedOut bit is 1, I will always return a future date. Otherwise, I return the
Membership did not have a way to say if one user is allowed to be locked out or not, so we don’t have this functionality.
Update Your Existing Code
Once again you can check how the MVC5 template does it, but basically you have to remove any usage of “
Membership” and “
FormsAuthentication“. You can also update your actions with async functionality.
Here is a table of the main things you will have to change:
Have a look on what is on the UserManager. You will find plenty of interesting methods.
What is Not Implemented?
- Claims: they do not exist in the Membership code base.
- Profile: it does exist but I did not implement it.
How Did I Do It?
I spent a good deal of time with the ILSpy decompiler looking at the Owin implementation. Understanding how everything was being called was definitely the key. I also created a dummy MVC5 application with some users and compared the databases with SQL Server 2014 Management Studio.
I’m not claiming the code is perfect here; I also did not implement everything. Feel free to send pull requests if you believe something is wrong.