Build Your Own Membership System For ASP.NET MVC - Part II

Tuesday, October 2, 2012

MemFlex is a look at what is possible in an ASP.NET MVC application if you eschew the existing ASP.NET providers for membership, roles, and OAuth. MemFlex doesn’t use the existing providers, but does use classes from the .NET framework and DotNetOpenAuth to build a membership and roles system with some simple requirements:

- Support all the actions of the MVC 4 Internet AccountController (register, login, login with OAuth).

- Be test friendly (by providing interface definitions for both clients and dependencies)

- Run without ASP.NET (for integration tests and database migrations, as two examples).

- Work with a variety of data sources (two common scenarios for storing use accounts these days involve document databases and web services).

Here are parts of the project as they exist today.

Sample Application

The sample application is a typical MVC 4 Internet application where the AccountController and database migrations use the FlexMembershipProvider. There are classes provided for working with both the Entity Framework and RavenDB, and you can (almost) switch between the two by adjusting an assembly reference and the namespaces you use.

After you’ve defined what a User model should look like, the first part would be configuring a FlexMembershipUserStore to work with your custom user type.

using FlexProviders.EF;

namespace LogMeIn.Models
{
public class UserStore : FlexMembershipUserStore<User>
{
public UserStore(MovieDb db) : base(db)
{

}
}
}

The above code snippet, which uses EF, just requires a generic type parameter (your user type), and a DbContext object to work with. The UserStore is then plugged into the FlexMembershipProvider. You can do this by hand, or let an IoC container take care of the work.

var membership = new FlexMembershipProvider(
new UserStore(context),
new AspnetEnvironment());

Once the FlexMembershipProvider is initialized inside a controller, you can use an API that looks a bit like the traditional ASP.NET Membership API.

_membershipProvider.Login(model.UserName, model.Password

Everything Else

The FlexProviders part of the project consists of 4 pieces: the integrations tests, a RavenDB user store, an EF user store, and the FlexProviders themselves.

FlexProviders

The FlexProviders project defines the basic abstractions for a flexible membership system. For example, the interface definition to work with locally registered users (for now, a separate interface provides the OAuth functionality):

public interface IFlexMembershipProvider
{
bool Login(string username, string password);
void Logout();
void CreateAccount(IFlexMembershipUser user);
bool HasLocalAccount(string username);
bool ChangePassword(string username, string oldPassword, string newPassword);
}

There is also a concrete implementation of a flexible membership provider:

public class FlexMembershipProvider : IFlexMembershipProvider, 
IFlexOAuthProvider,
IOpenAuthDataProvider
{
public FlexMembershipProvider(
IFlexUserStore userStore,
IApplicationEnvironment applicationEnvironment)
{
_userStore = userStore;
_applicationEnvironment = applicationEnvironment;
}

public bool Login(string username, string password)
{
var user = _userStore.GetUserByUsername(username);
if(user == null)
{
return false;
}

// ... omitted for brevity ...
}
// ...
}

Of course since the membership provider requires an IFlexUserStore dependency, the operations required for data access are defined in this project in the IFlexUserStore interface. There is also an AspnetEnvironment class that removes hard dependencies on test unfriendly bits like HttpContext.Current.

public class AspnetEnvironment : IApplicationEnvironment
{
public void IssueAuthTicket(string username, bool persist)
{
FormsAuthentication.SetAuthCookie(username,persist);
}

// ...
}

FlexProviders.EF and FlexProviders.Raven

It’s relatively straightforward to build classes that will take care of the data access required by a membership provider. For both Raven and EF, all you really need is a generic type parameter and a unit of work. For Raven:

namespace FlexProviders.Raven
{
public class FlexMembershipUserStore<TUser>
: IFlexUserStore where TUser : class, new()

{
private readonly IDocumentSession _session;

public FlexMembershipUserStore(IDocumentSession session)
{
_session = session;
}

public IFlexMembershipUser GetUserByUsername(string username)
{
return _session.Query<TUser>().SingleOrDefault(u => u.Username == username);
}

// ...
}
}

And the EF version:

namespace FlexProviders.EF
{
public class FlexMembershipUserStore<TUser>
: IFlexUserStore where TUser: class, IFlexMembershipUser, new()
{
private readonly DbContext _context;

public FlexMembershipUserStore (DbContext context)
{
_context = context;
}

public IFlexMembershipUser GetUserByUsername(string username)
{
return _context.Set<TUser>().SingleOrDefault(u => u.Username == username);
}

// ...
}
}

FlexProviders.Tests

This project is a set of integration tests to verify the EF and Raven providers actually put and retrieve data with real databases. The EF tests require the DTC to be running (net start msdtc). The tests are configured to use SQL 2012 LocalDB (for EF) by default, while the Raven tests use Raven’s in-memory embedded mode.

None of the code is vetted or hardened and still needs some work, but if you find it to be an inspiration or have some feedback or pull requests, let me know.

There are no NuGet packages available, as yet.

Conclusion

I said in the last post that building a membership system is easy if you know exactly what you want. You can mostly rely on other pieces of the framework for the hard parts (creating secure cookies and cryptography, for example, but also relying on DotNetOpenAuth for the OAuth heavy lifting).

The hard part of building a membership system is when you try to build it for unknown customers and unknown scenarios. I don’t envy Microsoft in the sense that if they build a membership system that is simple, 60% of their customers will say it doesn’t work for their application. If they build a membership system that is sophisticated enough to work with most applications, 60% of their customers will say it looks like WCF. Somewhere there is a sweet spot that will make a majority of customers happy.

What I have here will work for most of the applications I’ve been associated with, provides some flexibility in the data store, and still remains, I think, relatively easy to understand.


Comments
gravatar Ignacio Fuentes Tuesday, October 2, 2012
Great Post Scott,
One very common scenario you dont often see mentioned is how to expose your own WebApi with OAuth.
Im guessing building a OAuth provider is much harder than an OAuth client. Especially if you want your app to be both client/provider.
gravatar Scott Wednesday, October 3, 2012
Yes, I think so, too.
gravatar Jesse Wednesday, October 3, 2012
This might be a kind of a rookie question, but why is IFlexRole generic (in FlexProviders.Raven)? The Type parameter doesn't seem to get used, unless I missed it.
gravatar scott Wednesday, October 3, 2012
@Jesse: No, not a rookie question at all. It's a part of the design that sticks out and I'm not entirely happy with it yet. IFlexRole is there as a generic constraint so the data store knows there is Name and Users available to use in a query (.Single(r=>r.Name == name).

Would like to think of a better way to do that without forcing an interface or base class on people. Plus, I should test what happens with an explicit interface implementation (I think it would break the LINQ queries for Raven and EF).
gravatar Abby Friday, October 5, 2012
very informative post indeed.. being enrolled in www.wiziq.com/course/57-fresher-training-projects was looking for such articles online to assist me.. and your post helped me a lot
gravatar Jacob Handaya Sunday, October 7, 2012
Thank for the post, i tried to implement it with facebook

It give me an error when working with FlexMem


The specified type member 'OAuthAccounts' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.

my version is DotNetOpenAuth.OAuth.Core.4.0.3.12153

gravatar Jacob Handaya Sunday, October 7, 2012
Hi sorry it was my fault,

It works when use the Model 1st approach the problem occur when i was trying to change it to DB 1st approach, and forgot to include the FlexOAuthAccount



gravatar John Thursday, October 25, 2012
Hi, really liking this approach. I never have liked the membership provider, so nice work.

I think this the default behavior for oauth auth in webmatrix allows disassociation of an oauth account when there is > 1 oauth accounts. It looks like this is the same behavior in your system, I believe.

I think you should be able to remove an oauth account if there is a local account. i.e.

if (this.HasLocalAccount(user.Username))
return _userStore.DeleteOAuthAccount(provider, providerUserId);
if (accounts.Count() > 1)
return _userStore.DeleteOAuthAccount(provider, providerUserId);


Do you think this would be advisable? I'm trying to consider these stories before implementing. I may have time to contribute to this project if you need additional tests, etc. Thanks
Julien Friday, October 26, 2012
Hi,

Thanks for this great approach!

I have a question concerning the sample website
Why do you use Authorize in AccountController and then FlexAuthorize in AdminController?

Thanks a lot!
Comments are now closed.
by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!