Introduction
This article should really be called, everything you ever wanted to know about why SimpleMembershipProvider
does not work as a MembershipProvider
.
Never trust a class with the word simple in it's name, it sets expectations that are rarely met, and the SimpleMembershipProvider
is no exception. The new provider may extend from the MembershipProvider
abstract class, but it certainly cannot be used with the older membership system for the following reasons:
- Simple providers must be explicitly/implicitly configured as default providers
- MembershipUser isn't fully implemented, and only UserId/Username properties are mapped
- Membership based IsApproved/IsLockedOut functions aren't supported/mapped to new APIs
- Membership's ValidateUser returns true in the case of a locked out account
- Core functions of membership will result in a
NotSupportedException
For most projects these points probably don't matter, however there are times when this becomes an issue. For instance when you need to leverage the plugable nature of providers for integration into third party systems, such as a CMS.
My own experience relates to using SimpleMembershipProvider
with Sitecore. We wanted to leverage new features of the simple provider on the front-end, but still use Sitecore's User Manager features on the back-end. The good news is that it is possible to overcome these issues, and in a later article I'll open source my own alternative providers.
Background
The SimpleMembershipProvider
was introduced by the WebMatrix team, and is touted as the future of ASP.NET membership. In reality membership will soon be superseded by ASP.NET Identity. In the mean time simple providers move away from the Membership/Roles
interfaces, and introduces a cleaner WebSecurity
API. In doing so it breaks away from the traditional username/password based authentication scheme, and introduces support for federated authentication.
The new WebSecurity
framework also ties in with Entity Framework to provide an extendable entity model with support for Code-First, and can even bind to existing user tables/schema. The removal of Stored Procedures from the system means support for all SQL Server based products, including Azure and Sql Server CE.
It's worth noting that support for extended SQL Server products is also available with the new Universal Providers, but these lack the further enhancements described above and work only on a pre-defined membership schema.
To see SimpleMembershipProvider
in action, crack open Visual Studio and create a new instance of the MVC 4 Internet Application template. You'll notice the AccountController
exclusively uses the new WebSecurity
class which in turn makes use of the new ExtendedMembershipProvider
definition which SimpleMembershipProvider
derives from.
You may also expect to see the SimpleMembershipProvider
registered in the membership
section of the Web.Config
, however this is not the case.
The Simple Magic
The supposedly simple SimpleMembershipProvider
comes with a degree of magic. Sigh... why wouldn't it? It is possible to register the provider explicitly via the Web.Config
, however the new WebSecurity
API may also do some auto-wiring behind the scenes. This is controlled via the enableSimpleMembership
appSetting, and the WebSecurity.InitializeDatabaseConnection
routine.
Regardless of how SimpleMembershipProvider
is registered, any attempt to use the SimpleMembershipProvider
without first calling WebSecurity.InitializeDatabaseConnection
will result in the following exception:
You must call the "WebSecurity.InitializeDatabaseConnection" method before you call any other method of the "WebSecurity" class. This call should be placed in an _AppStart.cshtml file in the root of your site.
The reason the provider must be initialised is so that the application can map to any custom database schema, and to enable Entity Framework to initialise the database for Code-First.
If enableSimpleMembership
is set to true
in the Web.Config
, then WebSecurity
will override any default providers with SimpleMembershipProvider
and SimpleRoleProvider
.
If enableSimpleMembership
is not set, then WebSecurity
will attempt to initialise any existing ExtendedMembershipProvider
or ExtendedRoleProvider
instances configured via the Web.Config
, but only where they are configured as the default provider.
If enableSimpleMembership
is set, but WebSecurity.InitializeDatabaseConnection
is not called, then the simple providers will wrap & replace any existing AspNetSqlMembershipProvider/AspNetSqlRoleProvider
instances. In this case extended features available on the WebSecurity
API will result in the not initialised exception. Calls to the older membership APIs will pass through to the wrapped AspNetSql providers.
This leads to the first issue:
Simple providers must be explicitly/implicitly configured as default providers.
In most cases this is not a problem, but for systems like Sitecore which can use multiple providers simultaneously it's a real blocker.
Compatibility
The next problem is compatibility with the old membership system. To support WebSecurity
the new system requires an instance of ExtendedMembershipProvider
which extends from MembershipProvider
. However, although the simple provider is a MembershipProvider
in it's own right, the older API is somewhat broken if used as such.
The reason for this is that many of the older membership database columns have been removed, such as LastLoginDate
. In addition WebSecurity
supports a new token based account confirmation function, and introduces new password lockout behaviour that isn't naturally compatible with the older system.
The consequence of these changes is that the MembershipUser
returned when using the older APIs now only supports UserId/UserName. Other properties such as Email, IsApproved, CreatedDate, IsLockedOut are hard-wired to return default values. When used as designed it's not an issue, as the new system relies on Entity Framework models rather than MembershipUser
to expose data back to the application. It's only when SimpleMembershipProvider
is used in the context of MembershipProvider
that functionality is lost. For instance, the ability to unlock a locked account, or even the ability to create a new user account. These scenarios are now only possible via the WebSecurity
APIs, and plugging the provider into an existing user management interface written for the old system won't work.
Login Beware
Related to compatibility issues described above, there are additional consequences when using the SimpleMembershipProvider
in the context of MembershipProvider
.
As password lockout functions are now re-engineered to be based on a timeout and max retry count, it means the old system has no visibility on these states. As such ValidateUser
method will now return success even if the account is locked out. This is actually consistent with the new APIs which moves responsibility for checking the lockout state to the application, however it will certainly introduce security flaws when plugged into code built around the original membership system.
Incidentally the MVC 4 internet Application template is missing the locked out account check on login. This can be added as follows:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
ModelState.AddModelError("",
"The user name or password provided is incorrect.");
}
else if (WebSecurity.IsAccountLockedOut(model.UserName, 10, 3600))
{
ModelState.AddModelError("",
"You have entered a password incorrectly too many times. Try again in an hour.");
}
if (ModelState.IsValid && WebSecurity.Login(
model.UserName, model.Password, persistCookie: model.RememberMe))
{
return RedirectToLocal(returnUrl);
}
return View(model);
}
Missing Support
Lastly, the simple providers do not support many of the old APIs, and will return NotSupportedException
if called. Presumably these functions were too difficult to implement without Stored Procedures, or don't fit in with new schema/methodologies of the new design. Unsupported MembershipProvider
functions are:
- CreateUser
- GetUser (by providerUserKey)
- GetUserNameByEmail
- FindUserByUserName
- FindUserByEmail
- GetAllUsers
- FindUsersByName
- FindUsersByEmail
- UnlockUser
- ChangePasswordQuestionAndAnswer
- GetNumberOfUsersOnline
- GetPassword
- ResetPassword
Conclusion
On the surface it looks like SimpleMembershipProvider
provides the best of both worlds by extending on the existing MembershipProvider
. In reality there is no backwards compatibility of any value offered by this relationship, and security flaws could be easily introduced if used in this scenario.
In a future article I will provide a more compatible set of providers that solves a number of issues raised in this article, and will hopefully bridge the gap between the interfaces.