TitleAuthentication in ASP.NET MVC with SimpleMembership and OAuth

This blog post is about authentication and authorization of modern web solutions. Microsoft has been at the forefront of these developments, which also means things have been changing rapidly. Lots of technologies were developed, with “best practice” solutions going out of fashion very quickly. From Microsoft there is ASP.NET Forms Login (Membership and SimpleMembership), ADFS (1.0 and 2.0), WS-Federation, rel="noopener noreferrer" WS-Security, CardSpace, Microsoft Account (formerly Windows Live ID, Passport), Azure ACS (Access Control Services), Windows Identity Foundation (WIF) (version 3.5 rel="noopener noreferrer" and 4.5), WebPages OAuth (based rel="noopener noreferrer" on DotNetOpenAuth), Azure Mobile Services (Zumo) Authentication, Windows Azure Active Directory (WAAD) and Azure Authentication Library (AAL).

OAuth 2.0 logo

With all these technologies, it is easy to loose track of the essence, which is that security of modern websites evolves around rel="noopener noreferrer" the OAuth 2.0 standard. OAuth 2.0 is a standard that, after a long and painful process, was finalized in 2012. If you are new to OAuth 2.0, I recommend reading the OAuth 2.0 spec before starting to use it, the document is actually quite readable and answered many questions for me.

When implementing security for a modern ASP.NET MVC solution, there are essentially two options:

  • ASP.NET SimpleMembership OAuth. This solution seems to be preferred by the Microsoft ASP.NET team and is implemented in the most recent MVC4 Internet Website project templates.
  • WIF and WAAD/ADFS. This solution is being developed out of the Windows team, and is more geared toward organizations that want to centralize security for multiple websites and web services, who want to accommodate both internal (Windows) and external users or want federation between directories.

The first option is the easiest to understand. It is suited for implementing user login and security for a single, internet facing website, without the need to think or plan any further. In this post I will implement rel="noopener noreferrer" this on the Cloud Auction website.

In my previous blog post I created an ASP.NET MVC website using the Internet site template and set up the basics for the front end using Twitter Bootstrap and the Back End using the SqlFu Micro Orm. The MVC4 Internet Site template comes with a complete authentication solution based on WebPages OAuth. The template project references the required dependencies as NuGet packages, and contains a complete AccountController implementation, as source code so that these can be easily integrated with the rest of the site. And that is exactly what this post is about.

When I changed the template views to responsive (Bootstrap based) markup in Layout.cshtml during the initial setup of the project, the login feature of the original template was left out. To get it back I need to restore it by adding Login and Register links back into _NavBar.cshtml, and “bootstrappify” the markup in the views used by the Account controller.

Lets do that:


@helper LoggedInUser()
{
if (User != null && User.Identity != null && User.Identity.IsAuthenticated)
{
@:Hello, @Html.ActionLink(User.Identity.Name ?? "naam onbekend", "Manage", "Account")
}
else
{
@Html.ActionLink("Register", "Register", "Account")
}
}

@helper ActiveItem(string actionName, string controllerName, string areaName)
{
if (ViewContext.RouteData.Values["action"].ToString() == actionName &&
ViewContext.RouteData.Values["controller"].ToString() == controllerName &&
(ViewContext.RouteData.DataTokens["area"] == null || ViewContext.RouteData.DataTokens["area"].ToString() == areaName))
{
@:active
}
}


<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="brand" href="#">Cloud Auction</a>
<div class="nav-collapse collapse">
<ul class="nav">
<li class="@ActiveItem("Index", "Home", null)">@Html.ActionLink("Home", "Index", "Home")</li>
<li class="@ActiveItem("About", "Home", null)">@Html.ActionLink("About", "About", "Home")</li>
<li class="@ActiveItem("Contact", "Home", null)">@Html.ActionLink("Contact", "Contact", "Home")</li>
</ul>
@if (Request.IsAuthenticated)
{
<ul class="nav pull-right">
<li><div class="nav navbar-text">@LoggedInUser()</div></li>
<li class="divider-vertical"></li>
<li>
<div class="nav navbar-text">
@*<li><a href="javascript:document.getElementById('logoutForm').submit()">Log off</a></li>*@
@using (Html.BeginForm("LogOff", "Account", FormMethod.Post, htmlAttributes: new { id = "logoutForm", @class = "navbar-form" }))
{
<button type="submit" class="btn-link">Log Off</button>
@Html.AntiForgeryToken()
}
</div>
</li>
</ul>
}
else
{
<ul class="nav pull-right">
<li class="@ActiveItem("Register", "Account", null)">@Html.ActionLink("Register", "Register", "Account", new { ReturnUrl = this.Request.Url.AbsolutePath }, new { id = "registerLink", data_dialog_title = "Registration" })</li>
<li class="@ActiveItem("Login", "Account", null)">@Html.ActionLink("Login", "Login", "Account", new { ReturnUrl = this.Request.Url.AbsolutePath }, new { id = "loginLink", data_dialog_title = "Identification" })</li>
</ul>
}
</div><!--/.nav-collapse -->
</div>
</div>
</div>
view raw_NavBar.cshtml hosted with ❤ by GitHub
@model Auction.Web.Models.LoginModel
@{
ViewBag.Title = "Log in";
}

<h1>@ViewBag.Title <small>with local or social network account</small></h1>
<div class="row-fluid">
<div class="span6">
@using (Html.BeginForm(null, null, new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal well" }))
{
@Html.AntiForgeryToken()
@Html.ValidationSummary(true)

<fieldset>
<legend>using a local account</legend>
@Html.EditorFor(m => m.UserName, new { @class = "input-small" })

@Html.EditorFor(m => m.Password, new { @class = "input-small" })

@Html.EditorFor(m => m.RememberMe)

<input class="btn btn-primary" type="submit" value="Log in" />
&nbsp; or @Html.ActionLink("Register", "Register") if you don't have an account.
</fieldset>
}
</div>
<div class="span6">
@Html.Action("ExternalLoginsList", new { ReturnUrl = ViewBag.ReturnUrl })
</div>
</div> <!-- row-fluid -->
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
view rawLogin.cshtml hosted with ❤ by GitHub


We are using the EditorTemplates from the blogpost on forms and validation, rel="noopener noreferrer" so we can simply wite @Html.EditorFor(m => m.UserName) that will render the complete control group with label and error handling in bootstrap compatible markup.

In the post about SQL Migrations, I explained how we use Fluent Migrator to manage our database schema, and SqlFu as our Micro-ORM. Of course, the MVC Internet website template uses Entity Framework which I try to avoid. It also uses a weird InitializeSimpleMembership attribute on the AccountController where the SimpleMembership database connection is initialized using a filter attribute. This was probably done to improve startup time by delaying security initialization, but to me it looks strange and confusing, rel="noopener noreferrer" so I replaced it with a normal initialization, by doing the initialization in App_Start and calling WebSecurity.InitializeDatabaseConnection() there.

Because I like to develop from a known database starting point (deleting and re-creating the datadase everytime I run the application in the debugger, I also created a Migration for the webpages Membership database. I checked the sourcecode and created the exact same structure, using FluentMigrator. I preload the development data with a user account for myself so I don’t have to register every time.

The Migration for webpages SimpleMembership:


using System;
using FluentMigrator;

namespace Auction.Web.Domain.Migrations
{
[Migration(2)]
public class UserProfile : Migration
{
public override void Up()
{
Create.Table("UserProfile")
.WithColumn("UserId").AsInt32().Identity().PrimaryKey()
.WithColumn("UserName").AsString(100).NotNullable().Unique();

Create.Table("webpages_Roles")
.WithColumn("RoleId").AsInt32().NotNullable().PrimaryKey().Identity()
.WithColumn("RoleName").AsString(256).NotNullable().Unique();

Create.Table("webpages_UsersInRoles")
.WithColumn("UserId").AsInt32().NotNullable().ForeignKey("fk_UsersInRole_UserId", "UserProfile", "UserId")
.WithColumn("RoleId").AsInt32().NotNullable().ForeignKey("fk_UsersInRole_RoleId", "webpages_Roles", "RoleId");
Create.PrimaryKey("pk_webpages_UsersInRoles").OnTable("webpages_UsersInRoles").Columns(new[] { "UserId", "RoleId" });

Create.Table("webpages_Membership")
.WithColumn("UserId").AsInt32().NotNullable().PrimaryKey().ForeignKey("fk__Membership_UserId", "UserProfile", "UserId")
.WithColumn("CreateDate").AsDateTime()
.WithColumn("ConfirmationToken").AsString(128).Nullable()
.WithColumn("IsConfirmed").AsBoolean().WithDefaultValue(0)
.WithColumn("LastPasswordFailureDate").AsDateTime().Nullable()
.WithColumn("PasswordFailuresSinceLastSuccess").AsInt32().NotNullable().WithDefaultValue(0)
.WithColumn("Password").AsString(128).NotNullable()
.WithColumn("PasswordChangedDate").AsDateTime().Nullable()
.WithColumn("PasswordSalt").AsString(128).NotNullable()
.WithColumn("PasswordVerificationToken").AsString(128).Nullable()
.WithColumn("PasswordVerificationTokenExpirationDate").AsDateTime().Nullable();

Create.Table("webpages_OAuthMembership")
.WithColumn("Provider").AsString(30).NotNullable()
.WithColumn("ProviderUserId").AsString(100).NotNullable()
.WithColumn("UserId").AsInt32().NotNullable().ForeignKey("fk_OAuthMembership_UserId", "UserProfile", "UserId");
Create.PrimaryKey("pk_webpages_OAuthMembership").OnTable("webpages_OAuthMembership").Columns(new[] { "Provider", "ProviderUserId" });

Create.Table("webpages_OAuthToken")
.WithColumn("Token").AsString(100).NotNullable().PrimaryKey()
.WithColumn("Secret").AsString(100).NotNullable();
}

public override void Down()
{
Delete.Table("webpages_OAuthToken");
Delete.Table("webpages_OAuthMembership");
Delete.Table("webpages_UsersInRoles");
Delete.Table("webpages_Roles");
Delete.Table("webpages_Membership");
Delete.Table("UserProfile");
}
}
}
view rawMembershipMigration.cs hosted with ❤ by GitHub


There is only one place where the AccountController uses EntityFramework to directly access the database (a login scenario Microsoft probably missed when buiding WebPages OAuth) that can easily be refactored to use our way of accessing the rel="noopener noreferrer" database. After that, we can succesfully:

PM> Uninstall-Package “EntityFramework”

With that our security infrastructure is in place. We use exactly the parts that we want, and nothing more.

To test the social network login features, I will add a Facebook login to our site. To do this, you have to register as a developer with Facebook, which is easy. Go to https://developers.facebook.com/ and register yourself as a developer. Then click the Apps menu.

image



You need to register your site as a Facebook App to be able to use a Facebook login. Click the Create New App button to register your site with Facebook (dialog is in Dutch because that’s what Facebook gives me, being Dutch):

image

Solve the Captcha (the hardest part of this process), and you will be welcomed with the App registration screen. On top of the page you will find the App ID and the App Secret that need to be configured in the MVC App. Facebook needs to know the Url of your site, so that it can make sure to redirect users of your App only to your server. Because of how OAuth works, you should always only support it with https. OAuth is simply not secure with http.

With Facebook, you can enter localhost for the app domain and use (unsecure) http for development. But other login providers are more restrictive, so it is better to setup your development machine with a (self-signed) SSL certificate and use a dns alias. I have configured my computer that way, and enter my alias “auction.local.net” as the App Domain for my Facebook app. This can be changed later to the real production url when the site goes into testing.

image 
Take the App ID and App Secret from the registration page and put it in App_Start\AuthConfig.cs:

public static class AuthConfig { public static void RegisterAuth() { OAuthWebSecurity.RegisterFacebookClient( appId: "571531462886850", appSecret: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); } }
(Of course, in a real implementation this would use an AppSetting in web.config or something equivalent)

Now we can run our website in the local debugger and test our login and registration process. Users can use a local password or their Facebook account to login. These options are not mutually exclusive; a user can have multiple social accounts and a local password that all connect to the same user profile.
image

Let’s test Facebook as a user:

image



By default, the new app asks for permissions to public profile, email and friends list. These permissions are important when you start using the Facebook API, and can be modified via App Settings.

image

After the login on Facebook, the user is returned to our site on the Account/ExternalLoginCallback page where the user profile is associated with the Facebook account.

image

We choose a username for our site (the email we received from Facebook is the default name) and after confirmation we can see our login is working.

image

We can test the other pages and see that it is a fully working login system, with local and social networking accounts. This is a very desirable functionality and it is very nice that this all comes pre-packaged in the new MVC4 Internet Website template.

[This post is part of a series: Development of a mobile website with apps and social features]