ASP.NET Providerless Custom Forms Authentication, Roles and Profile with MongoDb

How to handle custom provider-less Membership, Role and Profile with MongoDb

Some time ago I wrote this article about how to store an entire User class, which could contain User Authentication, Role(s), and Profile information in a Forms Authentication ticket (cookie), thereby essentially eliminating the chatty paradigm of using an RDBMS such as SQL Server for Membership, Roles, and Profile. Of course the database still got hit, but only when a user needed to log in, since all the user-related information was being carried around with them neatly in their Forms Authentication cookie.

The article featured using LZMA (7Zip) compression to be able to pack more info into the cookie. Now that MongoDb is available and there are decent C# Drivers for it (I use the NoRM driver), it makes a lot more sense to store this UserData object directly into the database. Think of it this way: Your SQL Server is already probably working overtime because of all the other cool stuff you’re doing on your site or sites, so anytime you have the opportunity to take some load off the database by performing an often-used function somewhere else, you’re probably doing yourself a favor from a performance standpoint.

So what I’ve done here is to rewrite the approach using MongoDb as the back-end, and I’ve also replaced the LZMA compression with a more lightweight implementation of MiniLZO. Compression is really not even needed since you can store about 4000 characters of info in a cookie before it will blow up on you, (the typical serialized amount here is only about 900 bytes) but it’s there because it’s fast and gives you some extra breathing room to store “bigger” UserData classes containing more Profile info fields.

To put this to work, you’ll first need to download and install MongoDb as a Windows Service. I have a very simple 5 – step guide in this article, so if you want, go ahead and download the bits, install it, and then resume reading here.

A copy of the NoRM MongoDb driver is in my downloadable Visual Studio 2010 Solution, but you may also want to get a full copy of the source from Andrew Theken’s GitHub repository here. Just use the “Download Source” button at the top right of the page.

There are over 400 unit tests in the solution, so just about anything you want to learn how to do with the NoRM driver has a matching test that you can study.

This implementation uses the Application_AuthenticateRequest event, which is available in Global.asax, and which is fired for every Request. Let’s have a look at that code first:

protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
// this method is fired on every request. We use it to get our FormsAuth cookie and rehydrate
// the Stored UserData object, complete with Profile info
if (HttpContext.Current.User != null)
{
if (HttpContext.Current.User.Identity.IsAuthenticated)
{
if (HttpContext.Current.User.Identity is FormsIdentity)
{
// Get Forms Identity From Current User
FormsIdentity id = (FormsIdentity)HttpContext.Current.User.Identity;
// Get Forms Ticket From Identity object
FormsAuthenticationTicket ticket = id.Ticket;
// Retrieve stored user-data object from Ticket cookie
string userData = ticket.UserData;
// uncompress and deserialize the compressed UserData instance:
CustomAuth.UserData u = Auth.ConvertCompressedStringToUserData(userData);
// assign our previously compressed UserData instance to the Current.User property
HttpContext.Current.User =(IPrincipal) u;
}
}
}

This code simply looks to see if the Request is Authenticated with a FormsIdentity, and if so, grabs the userData from the forms ticket and converts it to my UserData class which conveniently attaches to the User property of the current HttpContext. This is therefore now carried around the site with the user during their travels. All standard stuff, we’re just making use of how ASP.NET Forms Authentication works. Forms Authentication supports a UserData property, and the HttpContext already has a User property; no need to reinvent the wheel. As long as your User object implements IPrincipal, which is a no-brainer, you're good to go. You can have as many additional user/role/profile properties as you want in it.

Now let’s switch over to the “Join” or “Register” page to see how this data actually gets in there. Here is 100% of the logic when you fill out the form and press the submit button:

protected void btnSubmit_Click(object sender, EventArgs e)
{
try
{
string userName = txtUserName.Text;
string password = txtPassword.Text;
string email = txtEmail.Text;
string userUrl = txtUserUrl.Text;
if (!userUrl.ToLower().StartsWith("http://")) userUrl = "http://" + userUrl;
bool chkNews = this.chkNewsLetter.Checked;
bool chkNotify = this.chkNotification.Checked;
Guid gooid = Guid.NewGuid();
string userBio = txtBio.Text;
IIdentity ident = new GenericIdentity(userName);
string[] roles = {"user"};
string extraData = ""; // use this field for additional data
UserData uData =
new CustomAuth.UserData(gooid,ident, roles, userName , password,email ,chkNews,
chkNotify , userUrl , userBio ,extraData);
string cpUserData = Auth.ConvertUserDataToCompressedString(uData);
uData._roles = new string[]{cpUserData};


using (Mongo mongo = Mongo.Create(MongoHelper.ConnectionString()))
{
Norm.Collections.MongoCollection<UserData> coll =
(MongoCollection<UserData>)mongo.GetCollection<UserData>();
coll.Insert(uData);
}
lblMessage.Text = "<b>You have joined!</b> <br/>Please <a href=\"Default.aspx\"> Log in </a>.";
}
catch(Exception ex)
{
lblMessage.Text = ex.Message;
}
}

And here is the UserData Class that is being populated:

using System;
using System.Security.Principal;
using Norm;
using Norm.Attributes;

namespace CustomAuth
{
[Serializable]
public class UserData : IPrincipal
{
[MongoIdentifier]
public Guid ID { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public string Email { get; set; }
public bool Notification { get; set; }

public string UserUrl { get; set; }
public bool Newsletter { get; set; }

public string UserBio { get; set; }

public string[] _roles { get; set; }
public string ExtraData { get; set; }
private IIdentity _identity;

public UserData()
{
}

public UserData(Guid id, IIdentity identity, string[] roles, string userName, string password, string email, bool notification,
bool newsLetter, string userUrl, string userBio, string extraData)
{
_identity = identity;
_roles = new string[roles.Length];
roles.CopyTo(_roles, 0);
Array.Sort(_roles);

UserName = userName;
Password = password;
Email = email;
Newsletter = newsLetter;
Notification = notification;
ID = id;
UserUrl = userUrl;
UserBio = userBio;
ExtraData = extraData;
}


public bool IsInRole(string role)
{
return Array.BinarySearch(_roles, role) >= 0 ? true : false;
}

// This tells MongoDb to ignore this property. It doesn't like interfaces, and we don't need it stored anyway.
// Implementing IPrincipal allows us to attach this to the HttpContext.
[MongoIgnore]
public IIdentity Identity
{
get { return _identity; }
}

public bool IsInAllRoles(params string[] roles)
{
foreach (string searchrole in roles)
{
if (Array.BinarySearch(_roles, searchrole) < 0)
return false;
}
return true;
}

public bool IsInAnyRoles(params string[] roles)
{
foreach (string searchrole in roles)
{
if (Array.BinarySearch(_roles, searchrole) > 0)
return true;
}
return false;
}
}
}

The Login page (which in this simple demo is the Default.aspx page) has this logic:

protected void Button1_Click(object sender, System.EventArgs e)
{
// Initialize FormsAuthentication (reads the configuration and gets
// the cookie values and encryption keys for the given application)
FormsAuthentication.Initialize();
UserData foundUser = null;
// see if the person logging in is in our Users Collection:
using (Mongo mongo = Mongo.Create(MongoHelper.ConnectionString()))
{
Norm.Collections.MongoCollection<UserData> coll =
(MongoCollection<UserData>)mongo.GetCollection<UserData>();
UserData u = new UserData() {UserName = this.txtUserName.Text, Password = this.txtPassword.Text};
// You can do this in a simpler way, I just wanted to illustrate LINQ with AsQueryable
var list =
coll.AsQueryable().Where(
x => x.UserName == this.txtUserName.Text && x.Password == this.txtPassword.Text).ToList();

if (list.Count > 0)
foundUser = list[0];
}

if (foundUser==null)
{
Label1.Text = "Not Found. Please Join first.";
return;
}
// remember we have the entire UserData class in the roles property
string cpUserData = foundUser._roles[0];
// Create a new ticket used for authentication
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1, // Ticket version
txtUserName.Text, // Username to be associated with this ticket
DateTime.Now, // Date/time issued
DateTime.Now.AddMinutes(30000), // Date/time to expire
true, // "true" for a persistent user cookie (could be a checkbox on form)
cpUserData, // User-data (the string roles column from this user record in our database)
FormsAuthentication.FormsCookiePath); // Path cookie is valid for
// Hash the cookie for transport over the wire
string encTicket = FormsAuthentication.Encrypt(ticket);
HttpCookie cookie = new HttpCookie(
FormsAuthentication.FormsCookieName, // Name of auth cookie (it's the name specified in web.config)
encTicket); // Hashed ticket
cookie.HttpOnly = false; // we want a persistent cookie
cookie.Expires = DateTime.Now.AddDays(365);
// Add the cookie to the list for outbound response
Response.Cookies.Add(cookie);
// Redirect to requested URL, or homepage if no previous page requested
string returnUrl = Request.QueryString["ReturnUrl"];
if (returnUrl == null) returnUrl = "default.aspx";
// Don't call the FormsAuthentication.RedirectFromLoginPage method since it could
// replace the authentication ticket we just added...
Response.Redirect(returnUrl +"?msg=" +txtUserName.Text +"logged in.");
}

For production you'll want to add an index on the username and password fields for faster lookup.

And finally, we have an EditProfile.aspx page, that lets a user fill in additional Profile – type data and saves it:

protected void btnSave_Click(object sender, EventArgs e)
{
UserData uDataCurrent = (UserData)Page.User;
try
{
string userName = txtUserName.Text;
string email = txtEmail.Text;
string userUrl = txtUserUrl.Text;
string password = txtPassword.Text;
if (!userUrl.ToLower().StartsWith("http://")) userUrl = "http://" + userUrl;
bool chkNews = this.chkNewsLetter.Checked;
bool chkNotify = this.chkNotification.Checked;
Guid gooid = uDataCurrent.ID;
string userBio = txtBio.Text;
IIdentity ident = new GenericIdentity(userName);
string[] roles = { "user" };
string extraData = txtExtraData.Text; // use this field for additional data
UserData uData =
new CustomAuth.UserData(gooid, ident, roles, userName, password,email, chkNews,
chkNotify, userUrl, userBio, extraData);
string cpUserData = Auth.ConvertUserDataToCompressedString(uData);
uData._roles = new string[] {cpUserData};
using (Mongo mongo = Mongo.Create(MongoHelper.ConnectionString()))
{
Norm.Collections.MongoCollection<UserData> coll =
(MongoCollection<UserData>)mongo.GetCollection<UserData>();
coll.Save(uData);
}
lblMessage.Text = "Data saved. Return to <a href=default.aspx>Home page</a> </br> ";
}
catch (Exception ex)
{
lblMessage.Text = ex.Message;
}
}

To use Role information, you can employ code such as the following:

protected void Page_Load(object sender, EventArgs e)
{
if ( User.Identity.IsAuthenticated && User.Identity.Name !=null )
{
bool role = User.IsInRole("user");
lblnewmessage.Text = "User " + User.Identity.Name + " logged in. User Role:" +role.ToString( );
hlEditProfile.Visible = true;
}
}


That’s everything – Custom provider-less Forms Authentication with Roles and Profile, all stored in the Forms cookie, with a MongoDb back-end! You can download the full solution here.

By Peter Bromberg   Popularity  (8419 Views)