Introduction
Code generated by Visual Studio 2013 for an ASP.NET MVC5 website is great to get one started off quickly. Its now easy to build a great looking and highly maintainable website because of the excellent implementation of Model-View-Controller pattern. Now let's see how we can replace default the Entity Framework based user management and authentication model with a custom one.
Background
I have been spending a couple of days trying to migrate my existing PHP Yii based personal website to ASP.NET. Since the Yii framework is an MVC framework, I decided to use MVC with ASP.NET too. Coming to the point, as far as authentication is concerned, my website needs very little of it. In fact, this whole exploration into the ASP.NET MVC5 authentication system was because of a single requirement. I needed a way to execute the following in the 'Administration' section of the site.
If (username != "admin" or password != "mypassword") go to login page. That's it. I just need a single hard-coded user and password to protect a part of the site and nothing more.
However, because of the intrinsically rich functionality offered around authentication / user maintenance by ASP.NET MVC, implementing my simple requirement required me to learn a bit more than what I had originally imagined.
Using the Code
For starters, I have not found much material on this, so I started by understanding which classes and methods are really needed for what I had in mind. Below is a diagram which lists the main classes which we need to work with.
The out-of-the-box authentication involves the classes ApplicationUser
, ApplicationUserManager
, ApplicationSignInManager
and the UserStore
class. The pattern is that the UserStore
class is responsible to storing / retrieving instances of ApplicationUser
. The ApplicationUserManager
& the ApplicationSignInManager
makes use of UserStore
class to verify passwords, check account lockedout status and others. As part of our requirement, we will be replacing the ApplicationUser
and UserStore
classes with very basic user and userstore classes.
So, the classes which are implemented from scratch are:
SimpleUser
- This replaces the ApplicationUser
class which is generated by the IDE.
XmlUserStore
- This replaces the built in UserStore
class which is used by the generated code.
Just to add a little bit more sophistication than checking user != admin || password != password, XmlUserStore
will be storing user ids and passwords in an XML file. Maintenance of this XML file is not part of the requirement, XmlUserStore
is only responsible for reading and handing off user ids and passwords. If we want additional functions like add-new, modify, delete, etc., we need to implement those methods. We will encounter some of them but these only throw NotImplementedException
for now.
The important thing is that we implement the correct interfaces and provide full implementation for only those methods we are interested in.
SimpleUser
implements the basic IUser
interface and has a GenerateUserIdentityAsync
method.
- The
XmlUserStore
class implements IUserStore<SimpleUser>
, IUserPasswordStore<SimpleUser>
and the IUserLockoutStore<SimpleUser, object>
interfaces.
- The important methods of
XmlUserStore
are:
IUserStore
's FindByIdAsync(string userId)
IUserStore
's FindByNameAsync(string userName)
IUserPasswordStore
's GetPasswordHashAsync(SimpleUser user)
IUserLockoutStore
's GetLockoutEnabledAsync(SimpleUser user)
These are all that is needed to achieve what we want to do. Once these two classes are in place, we need to modify the IDE generated code to use these classes instead of the built in User
and UserStore
class. The App_Start
/IdentityConfig.cs file is where we start the modifications. The ApplicationUserManager
is at the heart of the authentication work. We first change its derivation to:
public class ApplicationUserManager : UserManager<SimpleUser>
For the purpose of understanding, the following methods have been implemented freshly in the ApplicationUserManager
to log the call but ultimately call the base class' implementation:
public override Task<SimpleUser> FindAsync(string userName, string password)
public override Task<SimpleUser> FindByEmailAsync(string email)
public override Task<SimpleUser> FindByNameAsync(string userName)
protected override Task<bool> VerifyPasswordAsync(IUserPasswordStore<SimpleUser, string> store,
SimpleUser user, string password)
public override Task<bool> IsLockedOutAsync(string userId)
public override Task<bool> GetTwoFactorEnabledAsync(string userId)
public override Task<ClaimsIdentity> CreateIdentityAsync(SimpleUser user, string authenticationType)
The static
method Create()
creates the ApplicationUserManager
class and also initializes the XmlUserStore
which needs the credentials-xml-file to be specified in the constructor. The password is hashed using ASP.NET's PasswordHasher
class offline and stored in the file. The file content looks like this:
="1.0" ="utf-8"
<users>
<user id=pop@pop.com
password="ACWUFWmyVcWeLugKyLOrZJzDUpqmmUzeDZ+w0/Dbs2YAy7Xj5FfmdS65GRXX8lLTDw==" />
</users>
To test the sample WebApplication
, use the login: pop@pop.com and password pop.
The next important class to modify is the ApplicationSignInManager
. We change its derivation to:
public class ApplicationSignInManager : SignInManager<SimpleUser, string>
For the purpose of understanding, the following methods have been implemented freshly in the ApplicationSignInManager
to log the call but ultimately call the base class' implementation:
public override Task<ClaimsIdentity> CreateUserIdentityAsync(SimpleUser user)
public override Task<SignInStatus> PasswordSignInAsync
(string userName, string password, bool isPersistent, bool shouldLockout)
public static ApplicationSignInManager Create
(IdentityFactoryOptions<ApplicationSignInManager> options, IOwinContext context)
Note, I have removed EmailService
, SmsService
classes from IdentityConfig.cs as they are not needed for this discussion.
The next file on our list is the App_Start/Startup.Auth.cs. We modify the public void ConfigureAuth(IAppBuilder app)
method and use the SimpleUser
class here:
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, SimpleUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) =>
user.GenerateUserIdentityAsync(manager))<applicationusermanager, simpleuser="">
Since we are not supporting adding, editing, and other management tasks related to users, all related methods have been deleted from the AccountController.cs file. The only important methods are Login()
and LogOff()
.
We now have a custom authentication working with ASP.NET MVC's authentication framework.
Points of Interest
It's not that we absolutely must implement a UserStore
class since the ApplicationUserManager
methods just delegate calls to corresponding methods in the UserStore
class but from the separation of concerns point of view, it's probably better we stick to the design the MVC guys choose for us.