Customize the SimpleMembership in ASP.NET MVC 4.0
As we know, .NET 4.5 have come up to us, and come along with a lot of new interesting features as well. Visual Studio 2012 was also introduced some days ago. They made us feel very happy with cool improvement along with us. Performance when loading code editor is very good at the moment (immediate after click on the solution). I explore some of cool features at these days. Some of them like Json.NET integrated in ASP.NET MVC 4.0, improvement on asynchronous action, new lightweight theme on Visual Studio, supporting very good on mobile development, improvement on authentication…
I reviewed them, and found out that in this version of .NET Microsoft was not only developed new feature that suggest from community but also focused on improvement performance of existing features or components. Besides that, they also opened source more projects, like Entity Framework, Reactive Extensions, ASP.NET Web Stack… At the moment, I feel Microsoft want to open source more and more their projects.
Today, I am going to dive in deep on new SimpleMembership model. It is really good because in this security model, Microsoft actually focus on development needs. As we know, in the past, they introduce some of provider supplied for coding security like MembershipProvider, RoleProvider… I don’t need to talk but everyone that have ever used it know that they were actually hard to use, and not easy to maintain and unit testing. Why? Because every time you inherit it, you need to override all methods inside it. Some people try to abstract it by introduce more method with virtual keyword, and try to implement basic behavior, so in the subclass we only need to override the method that need for their business. But to me, it’s only the way to work around. ASP.NET team and Web Matrix knew about it, so they built the new features based on existing components on .NET framework. And one of component that comes to us is SimpleMembership and SimpleRole. They implemented the Façade pattern on the top of those, and called it is WebSecurity. In the web, we can call WebSecurity anywhere we want, and make a call to inside wrapper of it.
I read a lot of them on web blog, on technical news, on MSDN as well. Matthew Osborn had an excellent article about it at his blog. Jon Galloway had an article like this at here. He analyzed why old membership provider not fixed well to ASP.NET MVC and how to get over it.
Those are very good to me. It introduced to me about how to doing SimpleMembership on it, how to doing it on new ASP.NET MVC web application. But one thing, those didn’t tell me was how to doing it on existing security model (that mean we already had Users and Roles on legacy system, and how we can integrate it to this system), that’s a reason I will introduce it today. I have spent couples of hours to see what’s inside this, and try to make one example to clarify my concern. And it’s lucky that I can make it working well.
The first thing, we need to create new ASP.NET MVC application on Visual Studio 2012. We need to choose Internet type for this web application. ASP.NET MVC actually creates all needs components for the basic membership and basic role. The cool feature is DoNetOpenAuth come along with it that means we can log-in using facebook, twitter or Windows Live if you want. But it’s only for LocalDb, so we need to change it to fix with existing database model on SQL Server. The next step we have to make SimpleMembership can understand which database we use and show it which column need to point to for the ID and UserName. I really like this feature because SimpleMembership on need to know about the ID and UserName, and they don’t care about rest of it.
I assume that we have an existing database model like
So we will point it in code like
The codes for it, we put on InitializeSimpleMembershipAttribute like
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public sealed class InitializeSimpleMembershipAttribute : ActionFilterAttribute { private static SimpleMembershipInitializer _initializer; private static object _initializerLock = new object(); private static bool _isInitialized; public override void OnActionExecuting(ActionExecutingContext filterContext) { // Ensure ASP.NET Simple Membership is initialized only once per app start LazyInitializer.EnsureInitialized(ref _initializer, ref _isInitialized, ref _initializerLock); } private class SimpleMembershipInitializer { public SimpleMembershipInitializer() { try { WebSecurity.InitializeDatabaseConnection("DefaultDb", "User", "Id", "UserName", autoCreateTables: true); } catch (Exception ex) { throw new InvalidOperationException("The ASP.NET Simple Membership database could not be initialized. For more information, please see http://go.microsoft.com/fwlink/?LinkId=256588", ex); } } } }
And decorating it in the AccountController as below
[Authorize] [InitializeSimpleMembership] public class AccountController : Controller
In this case, assuming that we need to override the ValidateUser to point this to existing User database table, and validate it. We have to add one more class like
public class CustomAdminMembershipProvider : SimpleMembershipProvider { // TODO: will do a better way private const string SELECT_ALL_USER_SCRIPT = "select * from [dbo].[User]private where UserName = '{0}'"; private readonly IEncrypting _encryptor; private readonly SimpleSecurityContext _simpleSecurityContext; public CustomAdminMembershipProvider(SimpleSecurityContext simpleSecurityContext) : this(new Encryptor(), new SimpleSecurityContext("DefaultDb")) { } public CustomAdminMembershipProvider(IEncrypting encryptor, SimpleSecurityContext simpleSecurityContext) { _encryptor = encryptor; _simpleSecurityContext = simpleSecurityContext; } public override bool ValidateUser(string username, string password) { if (string.IsNullOrEmpty(username)) { throw new ArgumentException("Argument cannot be null or empty", "username"); } if (string.IsNullOrEmpty(password)) { throw new ArgumentException("Argument cannot be null or empty", "password"); } var hash = _encryptor.Encode(password); using (_simpleSecurityContext) { var users = _simpleSecurityContext.Users.SqlQuery( string.Format(SELECT_ALL_USER_SCRIPT, username)); if (users == null && !users.Any()) { return false; } return users.FirstOrDefault().Password == hash; } } }
SimpleSecurityDataContext at here
public class SimpleSecurityContext : DbContext { public DbSet<User> Users { get; set; } public SimpleSecurityContext(string connStringName) : base(connStringName) { this.Configuration.LazyLoadingEnabled = true; this.Configuration.ProxyCreationEnabled = false; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Configurations.Add(new UserMapping()); } }
And Mapping for User as below
public class UserMapping : EntityMappingBase<User> { public UserMapping() { this.Property(x => x.UserName); this.Property(x => x.DisplayName); this.Property(x => x.Password); this.Property(x => x.Email); this.ToTable("User"); } }
One important thing, you need to modify the web.config to point to our customize SimpleMembership
<membership defaultProvider="AdminMemberProvider" userIsOnlineTimeWindow="15"> <providers> <clear/> <add name="AdminMemberProvider" type="CIK.News.Web.Infras.Security.CustomAdminMembershipProvider, CIK.News.Web.Infras" /> </providers> </membership> <roleManager enabled="false"> <providers> <clear /> <add name="AdminRoleProvider" type="CIK.News.Web.Infras.Security.AdminRoleProvider, CIK.News.Web.Infras" /> </providers> </roleManager>
The good thing at here is we don’t need to modify the code on AccountController. We only need to modify on SimpleMembership and Simple Role (if need).
Now build all solutions, run it. We should see a screen like this
If I login to Twitter button at the bottom of this page, we will be transfer to twitter authentication page
You have to waiting for a moment
Afterwards it will transfer you back to your admin screen
You can find all source codes at my MSDN code. I will really happy if you guys feel free to put some comments as below. It will be helpful to improvement my code in the future. Thank for all your readings.