When i was working on my site, i was looking for a good authentication system solution, apparently that solution was right under my nose.
ASP.NET MVC 2 Built in authentication system.

 

This is a complete authentication system including User registration, logging in, Managing users and editing roles. I thought this will be a great solution as i did not need much from the authentication system.
Also this system can be easily transferable from project to project, and will run on any ASP.NET Server which is great!

 

I thought I’d share this experience, I have also edited the authentication system to comply better with IoC and MVC Concepts

 

There is a solution file at the end of this post for those wishing to just poke around.

First of all we need to create a new MVC 2 Example Application Project, this already contains all the files and references we need, apart from IoC and Spark View Engine implementation.

 

In order for the system to work with Castle we would need to have Castle working on our project, so lets get started!

 

There is a great castle tutorial at MVCSharp blog which i have used for this project to make things easier
If you already know how to set up castle in your project please do, but please note that i am going to be using the models and data classes from that example in order to keep everything unified.

 

After you are done with setting up Castle Windsor lets make sure it is working Navigate to Views/Home right click and click add a new view. Name the view as BookList make sure create strongly typed view is checked and under data type enter

IENumerable<ExampleProject1.Models.Book>
view raw example1.cs hosted with ❤ by GitHub

You should have the class Book in the models from the Castle Windsor tutorial
and make sure Select Master Page is checked and is pointing to Site.Master.
After the file has been created enter the following code  in the MainContent placeholder

<h2>BookList</h2>
<% foreach (var book in Model) { %>
<div>
Book Id: <%:book.Id%>
Book Author: <%:book.Author%>
Book Name: <%:book.Name%>
</div>
<% } %>
view raw example2.cs hosted with ❤ by GitHub

Now open up the Home Controller and add the following code:

private readonly IEBookRepository _bookRepository;
public HomeController(IEBookRepository bookRepository)
{
this._bookRepository = bookRepository;
}
view raw example3.cs hosted with ❤ by GitHub

Afterwards change the Index method return value to:

return View("BookList",_bookRepository.ListEBook());
view raw example4.cs hosted with ❤ by GitHub

Build and run the application

Now after we checked everything is working as intended its time to go to the next phase:
Adding the Spark View Engine Adding spark is very easy, again if you already know this skip to the next section. First add references for Spark.dll and Spark.Web.Mvc.dll

now in global.asax go to application_start method and add:

ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new SparkViewFactory());
view raw example5.cs hosted with ❤ by GitHub

in Web.config under ConfigSections add:
<section name="spark" type="Spark.Configuration.SparkSectionHandler, Spark"/>
view raw example6.cs hosted with ❤ by GitHub

and spark configuration node anywhere in the main configuration node(just like Castle)
<spark>
<compilation debug="true" />
<pages automaticEncoding="true">
<namespaces>
<add namespace="System.Collections.Generic"/>
<add namespace="System.Web.Mvc.Html"/>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web" />
<add namespace="System" />
<add namespace="Spark.Web.Mvc" />
<add namespace="Spark" />
<add namespace="ExampleProject1.Models" />
</namespaces>
</pages>
</spark>
view raw example6.xml hosted with ❤ by GitHub

Note that i added the namespace of our project models, we need to add every namespace to spark configuration if we want to use it without using the fully qualified name.

Next we’re going to edit the application views create a new file under Views/Shared called Application.spark this file is the same as Site.Master it is the master template for the rest of the site pages. Add the following code into Application.spark:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>
<use content="page-title"/>
</title>
<link href="../../Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="page">
<div id="header">
<div id="title">
<h1>My MVC Application</h1>
</div>
<div id="logindisplay">
<use file="LogOnUserControl.spark" />
</div>
<div id="menucontainer">
<ul id="menu">
<li>
${ Html.ActionLink("Home", "Index", "Home") }
</li>
<li>
${ Html.ActionLink("About", "About", "Home") }
</li>
</ul>
</div>
</div>
<div id="main">
<use content="main-content" />
<div id="footer">
</div>
</div>
</div>
</body>
</html>
view raw example7.aspx hosted with ❤ by GitHub

Next we’ll need to edit the partial view used here, without it the page won’t compile. change LogOnUserControl.aspcx to LogOnUserControl.spark and edit the contents of the file to fit to the code below:

<if condition="Request.IsAuthenticated">
Welcome <b> ${ Context.User.Identity.Name }</b>!
[ !{ Html.ActionLink("Log Off","LogOff", "Account") } ]
</if>
<else>
[ !{ Html.ActionLink("Log On","LogOn","Account") } ]
</else>
<span class="Apple-style-span" style="font-family: Arial, Verdana, sans-serif; white-space: normal; ">Now lets edit Index.aspx and BookList.aspx change both extensions to .spark and edit as follows: Index.spark</span>
view raw example8.aspx hosted with ❤ by GitHub

Now lets edit Index.aspx and BookList.aspx change both extensions to .spark and edit as follows:
Index.spark

<content name="page-title">
Home Page
</content>
<content name="main-content">
<h2>${ ViewData["Message"] }</h2>
<p>
To learn more about ASP.NET MVC visit <a href="http://asp.net/mvc" title="ASP.NET MVC Website">http://asp.net/mvc</a>.
</p>
</content>
view raw index.aspx hosted with ❤ by GitHub

BookList.spark
<viewdata model="IEnumerable<Book>" />
<content name="page-title">
BookList
</content>
<content name="main-content">
<h2>BookList</h2>
<div each="var book in Model">
Book Id: ${ book.Id } <br />
Book Author: ${ book.Author } <br />
Book Name: ${ book.Name } <br />
</div>
</content>
view raw example9.aspx hosted with ❤ by GitHub

You can now build and run the program and see that it runs correctly, note that i did not edit About.aspx since it serves no purpose for this demonstration.

Now once we have Castle Windsor and Spark ready for action, we can now begin what we’re really here for: MVC 2 Built in Authentication system.

 

First we need to convert the account login change password and register views to spark format: Edit ChangePassword.aspx to ChangePassword.spark and edit the contents to appear like this:

<viewdata model="ChangePasswordModel" PasswordLength="int" />
<use master="" />
<content name="page-title">
Change Password
</content>
<content name="main-content">
<h2>Change Password</h2>
<p>
Use the form below to change your password.
</p>
<p>
New passwords are required to be a minimum of ${ PasswordLength.ToString() } characters in length.
</p>
#using (Html.BeginForm())
#{
${ Html.ValidationSummary(true, "Password change was unsuccessful. Please correct the errors and try again.") }
<div>
<fieldset>
<legend>Account Information</legend>
<div class="editor-label">
${ Html.LabelFor(m => m.OldPassword) }
</div>
<div class="editor-field">
${ Html.PasswordFor(m => m.OldPassword) }
${ Html.ValidationMessageFor(m => m.OldPassword) }
</div>
<div class="editor-label">
${ Html.LabelFor(m => m.NewPassword) }
</div>
<div class="editor-field">
${ Html.PasswordFor(m => m.NewPassword) }
${ Html.ValidationMessageFor(m => m.NewPassword) }
</div>
<div class="editor-label">
${ Html.LabelFor(m => m.ConfirmPassword) }
</div>
<div class="editor-field">
${ Html.PasswordFor(m => m.ConfirmPassword) }
${ Html.ValidationMessageFor(m => m.ConfirmPassword) }
</div>
<p>
<input type="submit" value="Change Password" />
</p>
</fieldset>
</div>
#}
</content>
view raw example10.aspx hosted with ❤ by GitHub

Now edit ChangePasswordSuccess.aspx to ChangePasswordSucess.spark and edit the contents to match following code:
<use master="" />
<content name="page-title">
Change Password
</content>
<content name="main-content">
<h2>Change Password</h2>
<p>
Your password has been changed successfully.
</p>
</content>
view raw example11.aspx hosted with ❤ by GitHub

Edit LogOn.aspx to LogOn.spark and edit the contents to look like this:
<viewdata model="LogOnModel" />
<use master="" />
<content name="page-title">
Register
</content>
<content name="main-content">
<h2>Log On</h2>
<p>
Please enter your username and password. ${ Html.ActionLink("Register", "Register") } if you don't have an account.
</p>
#using (Html.BeginForm())
#{
!{ Html.ValidationSummary(true, "Login was unsuccessful. Please correct the errors and try again.") }
<div>
<fieldset>
<legend>Account Information</legend>
<div class="editor-label">
${ Html.LabelFor(m => m.UserName) }
</div>
<div class="editor-field">
${ Html.TextBoxFor(m => m.UserName) }
!{ Html.ValidationMessageFor(m => m.UserName) }
</div>
<div class="editor-label">
${ Html.LabelFor(m => m.Password) }
</div>
<div class="editor-field">
${ Html.PasswordFor(m => m.Password) }
!{ Html.ValidationMessageFor(m => m.Password) }
</div>
<div class="editor-label">
${ Html.CheckBoxFor(m => m.RememberMe) }
!{ Html.LabelFor(m => m.RememberMe) }
</div>
<p>
<input type="submit" value="Log On" />
</p>
</fieldset>
</div>
#}
</content>
view raw example12.aspx hosted with ❤ by GitHub

and the same for Register.aspx to Register.spark with the following content
<viewdata model="RegisterModel" PasswordLength="int" />
<use master="" />
<content name="page-title">
Register
</content>
<content name="main-content">
<h2>Create a New Account</h2>
<p>
Use the form below to create a new account.
</p>
<p>
Passwords are required to be a minimum of ${ PasswordLength.ToString() } characters in length.
</p>
#using (Html.BeginForm())
#{
!{ Html.ValidationSummary(true, "Account creation was unsuccessful. Please correct the errors and try again.") }
<div>
<fieldset>
<legend>Account Information</legend>
<div class="editor-label">
${ Html.LabelFor(m => m.UserName) }
</div>
<div class="editor-field">
${ Html.TextBoxFor(m => m.UserName) }
!{ Html.ValidationMessageFor(m => m.UserName) }
</div>
<div class="editor-label">
${ Html.LabelFor(m => m.Email) }
</div>
<div class="editor-field">
${ Html.TextBoxFor(m => m.Email) }
!{ Html.ValidationMessageFor(m => m.Email) }
</div>
<div class="editor-label">
${ Html.LabelFor(m => m.Password) }
</div>
<div class="editor-field">
${ Html.PasswordFor(m => m.Password) }
!{ Html.ValidationMessageFor(m => m.Password) }
</div>
<div class="editor-label">
${ Html.LabelFor(m => m.ConfirmPassword) }
</div>
<div class="editor-field">
${ Html.PasswordFor(m => m.ConfirmPassword) }
!{ Html.ValidationMessageFor(m => m.ConfirmPassword) }
</div>
<p>
<input type="submit" value="Register" />
</p>
</fieldset>
</div>
#}
</content>
view raw example13.aspx hosted with ❤ by GitHub

Now we’ll need to convert the files to fit a more MVC like approach.

First lets create the appropiate models, MVC 2 Authentication has wrapped all the models under 1 file, which i don’t like at all. so feel free to delete AccountModels.cs under Models directory.

Under models create a folder named Account and under that folder create a folder Validation in the validation folder create the following files:

AccountValidation.cs

public static class AccountValidation
{
public static string ErrorCodeToString(MembershipCreateStatus createStatus)
{
// See http://go.microsoft.com/fwlink/?LinkID=177550 for
// a full list of status codes.
switch (createStatus)
{
case MembershipCreateStatus.DuplicateUserName:
return "Username already exists. Please enter a different user name.";
case MembershipCreateStatus.DuplicateEmail:
return "A username for that e-mail address already exists. Please enter a different e-mail address.";
case MembershipCreateStatus.InvalidPassword:
return "The password provided is invalid. Please enter a valid password value.";
case MembershipCreateStatus.InvalidEmail:
return "The e-mail address provided is invalid. Please check the value and try again.";
case MembershipCreateStatus.InvalidAnswer:
return "The password retrieval answer provided is invalid. Please check the value and try again.";
case MembershipCreateStatus.InvalidQuestion:
return "The password retrieval question provided is invalid. Please check the value and try again.";
case MembershipCreateStatus.InvalidUserName:
return "The user name provided is invalid. Please check the value and try again.";
case MembershipCreateStatus.ProviderError:
return "The authentication provider returned an error. Please verify your entry and try again. If the problem persists, please contact your system administrator.";
case MembershipCreateStatus.UserRejected:
return "The user creation request has been canceled. Please verify your entry and try again. If the problem persists, please contact your system administrator.";
default:
return "An unknown error occurred. Please verify your entry and try again. If the problem persists, please contact your system administrator.";
}
}
}
view raw example14.cs hosted with ❤ by GitHub

PropertiesMustMatchAttribute.cs
public sealed class PropertiesMustMatchAttribute : ValidationAttribute
{
private const string _defaultErrorMessage = "'{0}' and '{1}' do not match.";
private readonly object _typeId = new object();
public PropertiesMustMatchAttribute(string originalProperty, string confirmProperty)
: base(_defaultErrorMessage)
{
OriginalProperty = originalProperty;
ConfirmProperty = confirmProperty;
}
public string ConfirmProperty { get; private set; }
public string OriginalProperty { get; private set; }
public override object TypeId
{
get
{
return _typeId;
}
}
public override string FormatErrorMessage(string name)
{
return String.Format(CultureInfo.CurrentUICulture, ErrorMessageString,
OriginalProperty, ConfirmProperty);
}
public override bool IsValid(object value)
{
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(value);
object originalValue = properties.Find(OriginalProperty, true /* ignoreCase */).GetValue(value);
object confirmValue = properties.Find(ConfirmProperty, true /* ignoreCase */).GetValue(value);
return Object.Equals(originalValue, confirmValue);
}
}
view raw example15.cs hosted with ❤ by GitHub

ValidatePasswordLengthAttribute.cs
public sealed class ValidatePasswordLengthAttribute : ValidationAttribute
{
private const string _defaultErrorMessage = "'{0}' must be at least {1} characters long.";
private readonly int _minCharacters = Membership.Provider.MinRequiredPasswordLength;
public ValidatePasswordLengthAttribute()
: base(_defaultErrorMessage)
{
}
public override string FormatErrorMessage(string name)
{
return String.Format(CultureInfo.CurrentUICulture, ErrorMessageString,
name, _minCharacters);
}
public override bool IsValid(object value)
{
string valueAsString = value as string;
return (valueAsString != null && valueAsString.Length >= _minCharacters);
}
}
view raw example16.cs hosted with ❤ by GitHub

Now close the validation folder and create the next classes in the Account folder:
ChangePasswordModel.cs
[PropertiesMustMatch("NewPassword", "ConfirmPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public class ChangePasswordModel
{
[Required]
[DataType(DataType.Password)]
[DisplayName("Current password")]
public string OldPassword { get; set; }
[Required]
[ValidatePasswordLength]
[DataType(DataType.Password)]
[DisplayName("New password")]
public string NewPassword { get; set; }
[Required]
[DataType(DataType.Password)]
[DisplayName("Confirm new password")]
public string ConfirmPassword { get; set; }
}
view raw example17.cs hosted with ❤ by GitHub

LogOnModel.cs
public class LogOnModel
{
[Required]
[DisplayName("User name")]
public string UserName { get; set; }
[Required]
[DataType(DataType.Password)]
[DisplayName("Password")]
public string Password { get; set; }
[DisplayName("Remember me?")]
public bool RememberMe { get; set; }
}
view raw example18.cs hosted with ❤ by GitHub

RegisterModel.cs
[PropertiesMustMatch("Password", "ConfirmPassword", ErrorMessage = "The password and confirmation password do not match.")]
public class RegisterModel
{
[Required]
[DisplayName("User name")]
public string UserName { get; set; }
[Required]
[DataType(DataType.EmailAddress)]
[DisplayName("Email address")]
public string Email { get; set; }
[Required]
[ValidatePasswordLength]
[DataType(DataType.Password)]
[DisplayName("Password")]
public string Password { get; set; }
[Required]
[DataType(DataType.Password)]
[DisplayName("Confirm password")]
public string ConfirmPassword { get; set; }
}
view raw example19.cs hosted with ❤ by GitHub

Now we need to add the service that puts it all together Create a Services folder and inside it create an Interfaces folder Inside the Interfaces folder create the following interfaces:

IFormsAuthenticationService.cs

public interface IFormsAuthenticationService
{
void SignIn(string userName, bool createPersistentCookie);
void SignOut();
}
view raw example20.cs hosted with ❤ by GitHub

IMembershipService
public interface IMembershipService
{
int MinPasswordLength { get; }
bool ValidateUser(string userName, string password);
MembershipCreateStatus CreateUser(string userName, string password, string email);
bool ChangePassword(string userName, string oldPassword, string newPassword);
}
view raw example21.cs hosted with ❤ by GitHub

Now lets implement these services Create the following classes in the Services folder:   FormsAuthenticationService.cs
public class FormsAuthenticationService : IFormsAuthenticationService
{
public void SignIn(string userName, bool createPersistentCookie)
{
if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName");
FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);
}
public void SignOut()
{
FormsAuthentication.SignOut();
}
}
view raw example22.cs hosted with ❤ by GitHub

MembershipService.cs
public class MembershipService : IMembershipService
{
private readonly MembershipProvider _provider;
public MembershipService()
: this(null)
{
}
public MembershipService(MembershipProvider provider)
{
_provider = provider ?? Membership.Provider;
}
public int MinPasswordLength
{
get
{
return _provider.MinRequiredPasswordLength;
}
}
public bool ValidateUser(string userName, string password)
{
if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName");
if (String.IsNullOrEmpty(password)) throw new ArgumentException("Value cannot be null or empty.", "password");
return _provider.ValidateUser(userName, password);
}
public MembershipCreateStatus CreateUser(string userName, string password, string email)
{
if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName");
if (String.IsNullOrEmpty(password)) throw new ArgumentException("Value cannot be null or empty.", "password");
if (String.IsNullOrEmpty(email)) throw new ArgumentException("Value cannot be null or empty.", "email");
MembershipCreateStatus status;
_provider.CreateUser(userName, password, email, null, null, true, null, out status);
return status;
}
public bool ChangePassword(string userName, string oldPassword, string newPassword)
{
if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName");
if (String.IsNullOrEmpty(oldPassword)) throw new ArgumentException("Value cannot be null or empty.", "oldPassword");
if (String.IsNullOrEmpty(newPassword)) throw new ArgumentException("Value cannot be null or empty.", "newPassword");
// The underlying ChangePassword() will throw an exception rather
// than return false in certain failure scenarios.
try
{
MembershipUser currentUser = _provider.GetUser(userName, true /* userIsOnline */);
return currentUser.ChangePassword(oldPassword, newPassword);
}
catch (ArgumentException)
{
return false;
}
catch (MembershipPasswordException)
{
return false;
}
}
}
view raw example23.cs hosted with ❤ by GitHub

Now we need to edit the account controller to reflect our changes, and also include the services in an IoC manner. Change AccountController.cs to appear like the code below:
public class AccountController : Controller
{
private readonly IFormsAuthenticationService formsService;
private readonly IMembershipService membershipService;
public AccountController(IFormsAuthenticationService formsService, IMembershipService membershipService)
{
this.formsService = formsService;
this.membershipService = membershipService;
}
// **************************************
// URL: /Account/LogOn
// **************************************
public ActionResult LogOn()
{
return View();
}
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
if (membershipService.ValidateUser(model.UserName, model.Password))
{
formsService.SignIn(model.UserName, model.RememberMe);
if (!String.IsNullOrEmpty(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
// **************************************
// URL: /Account/LogOff
// **************************************
public ActionResult LogOff()
{
formsService.SignOut();
return RedirectToAction("Index", "Home");
}
// **************************************
// URL: /Account/Register
// **************************************
public ActionResult Register()
{
ViewData["PasswordLength"] = membershipService.MinPasswordLength;
return View();
}
[HttpPost]
public ActionResult Register(RegisterModel model)
{
if (ModelState.IsValid)
{
// Attempt to register the user
MembershipCreateStatus createStatus = membershipService.CreateUser(model.UserName, model.Password, model.Email);
if (createStatus == MembershipCreateStatus.Success)
{
formsService.SignIn(model.UserName, false /* createPersistentCookie */);
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError("", AccountValidation.ErrorCodeToString(createStatus));
}
}
// If we got this far, something failed, redisplay form
ViewData["PasswordLength"] = membershipService.MinPasswordLength;
return View(model);
}
// **************************************
// URL: /Account/ChangePassword
// **************************************
[Authorize]
public ActionResult ChangePassword()
{
ViewData["PasswordLength"] = membershipService.MinPasswordLength;
return View();
}
[Authorize]
[HttpPost]
public ActionResult ChangePassword(ChangePasswordModel model)
{
if (ModelState.IsValid)
{
if (membershipService.ChangePassword(User.Identity.Name, model.OldPassword, model.NewPassword))
{
return RedirectToAction("ChangePasswordSuccess");
}
else
{
ModelState.AddModelError("", "The current password is incorrect or the new password is invalid.");
}
}
// If we got this far, something failed, redisplay form
ViewData["PasswordLength"] = membershipService.MinPasswordLength;
return View(model);
}
// **************************************
// URL: /Account/ChangePasswordSuccess
// **************************************
public ActionResult ChangePasswordSuccess()
{
return View();
}
}
view raw example24.cs hosted with ❤ by GitHub

Now Last but not least we need to add the namespaces to the new models we created to spark config in web.config
<add namespace="ExampleProject1.Models.Account" />
<add namespace="ExampleProject1.Models.Account.Validation" />
view raw example25.xml hosted with ❤ by GitHub

and adding the Services interfaces and implementations to castle configuration
<component id="FormsAuthenticationService" lifestyle="PerWebRequest" service="ExampleProject1.Services.Interfaces.IFormsAuthenticationService, ExampleProject1" type="ExampleProject1.Services.FormsAuthenticationService, ExampleProject1"></component>
<component id="MembershipService" lifestyle="PerWebRequest" service="ExampleProject1.Services.Interfaces.IMembershipService, ExampleProject1" type="ExampleProject1.Services.MembershipService, ExampleProject1"></component>
view raw example26.xml hosted with ❤ by GitHub

And thats it! we’re done!
You may now build and run and see that your authentication system is fully operational.
You may want to enable the roles system all you need to do is change a simple decleration in web.config  <roleManager enabled=”false”> from false to true, you can add roles using the visual studio tools or create a UI by yourself to edit these features.

As promised here is the solution file for this project: ExampleProject1.zip
Note: I have not included the castle and spark libraries as they are quite large in size.
I used Castle Windsor 2.1 and Spark View Engine 1.1 in this example.