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!
https://github.com/jsgoupil/membership2owin
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.Identity.Owin
- Microsoft.Owin.Host.SystemWeb
- Microsoft.AspNet.WebApi.Owin (If you are upgrading WebAPI)
Bring On the New Code!
Startup
You can start from an MVC5 application then bring in some code, or you can use the code I have put in GitHub. Everything starts with “Startup.cs” and “Startup.Auth.cs“.
Owin is using mainly three components: “SignInManager”, “UserManager”, and a “UserStore”. We will create our own here:
app.CreatePerOwinContext<SQLMembershipUserStore>(SQLMembershipUserStore.Create);
app.CreatePerOwinContext<SQLMembershipUserManager>(SQLMembershipUserManager.Create);
app.CreatePerOwinContext<SQLMembershipSignInManager>(SQLMembershipSignInManager.Create);
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.
- Extend
IUser<Guid>
- Implement that new interface
Id
should point to UserId
, and UserName
should point to aspnet_Users.UserName
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.
SignInManager
This file doesn’t have much to it, but you need to extend SignInManager
with your context like so:
SignInManager<aspnet_Membership, Guid>
The first generic is your context, the second generic is the UserId
key type.
UserManager
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.
UserStore
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
maxInvalidPasswordAttempts
is reached within thepasswordAttemptWindow
time 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 “LockoutEndDate
“. The FailedPasswordAttemptCount
used to work based on a time period window, I have changed it to simply be a counter like AccessFailedCount
.
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 FailedPasswordAttemptWindowStart
.
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:
Membership | Asp.Net Identity |
---|---|
FormsAuthentication.SignOut(); |
var ctx = Request.GetOwinContext(); |
FormsAuthentication.SetAuthCookie(username, true) |
var applicationUser = await UserManager.FindByNameAsync(username); |
Membership.ValidateUser(model.UserName, model.Password) |
var result = await SignInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, shouldLockout: true) |
Membership.GetUser(username) |
await UserManager.FindByNameAsync(model.UserName) |
Membership.CreateUser(username, password, login) |
UserManager.CreateUserAsync(aspnetMembershipInstance, password) |
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.