The other day I was working on a sample application and was trying to come up with an efficient way to handle authentication and authorization. I decided to consolidate this code by implementing custom principal and identity objects. I find this approach very flexible and concise. I wanted to document it for my own benefit and to gather some feedback if anyone is willing to share.
First of all, I am going to setup a brand new MVC project. I will pick internet template to get me a head start on creating basic authentication database and classes. Next I will run the application one time and register a user. Next, I am going to select Project->ASP.NET Configuration. I am going to enable roles, create a few and assign them to my user. Now my preparation work is done and it is rime to write some classes. If you ever looked at the interfaces IIdentity and IPrincipal, you probably already know what I am going to write, but the trick is that I am going to use membership provider to do authentication and roles provider to do authorization.
First of all, I am going to create some simple interfaces for both classes for general cleanliness and ability to mock both classes for tests:
using System.Security.Principal;
namespace MvcFormsAuth.Security
{
public interface ICustomIdentity : IIdentity
{
bool IsInRole(string role);
string ToJson();
}
}
using System.Security.Principal;
namespace MvcFormsAuth.Security
{
public interface ICustomPrincipal : IPrincipal
{
}
}
This part was simple. Now, the actual implementation. To authenticate I am going to use membership provider, but you can just as easily plug in custom implementation.
/// <summary>
/// Authenticate and get identity out with roles
/// </summary>
/// <param name="userName">User name</param>
/// <param name="password">Password</param>
/// <returns>Instance of identity</returns>
public static CustomIdentity GetCustomIdentity(string userName, string password)
{
CustomIdentity identity = new CustomIdentity();
if (Membership.ValidateUser(userName, password))
{
identity.IsAuthenticated = true;
identity.Name = userName;
var roles = System.Web.Security.Roles.GetRolesForUser(userName);
identity.Roles = roles;
return identity;
}
return identity;
}
As you can see, I am creating an instance of the object, but only setting the properties if membership provider confirms identity. I am also getting a list of roles and saving it off with identity. Of course, you can add some custom properties to it as well. My next step is come up with scheme to save this information in a cookie, thus being able to add custom information to the authentication cooking. To do so, I am going to use json based serialization. I am also going to create a custom class to pump my information into such as :
using System;
namespace MvcFormsAuth.Security
{
/// <summary>
/// Private members have short names to preserve space using json serialization
/// </summary>
public class IdentityRepresentation
{
private bool ia;
public bool IsAuthenticated
{
get { return ia; }
set { ia = value; }
}
private string n;
public string Name
{
get { return n; }
set { n = value; }
}
private string r;
public string Roles
{
get { return r; }
set { r = value; }
}
}
}
You noticed that field names are very short. I am doing this to save space during serialization process. There is a limit to how much information can be stored in a cookie, and every byte matters in this case. Here is what my serialization code looks like:
/// <summary>
/// Create serialized string for storing in a cookie
/// </summary>
/// <returns>String representation of identity</returns>
public string ToJson()
{
string returnValue = string.Empty;
IdentityRepresentation representation = new IdentityRepresentation()
{
IsAuthenticated = this.IsAuthenticated,
Name = this.Name,
Roles = string.Join("|", this.Roles)
};
DataContractJsonSerializer jsonSerializer =
new DataContractJsonSerializer(typeof(IdentityRepresentation));
using (MemoryStream stream = new MemoryStream())
{
jsonSerializer.WriteObject(stream, representation);
stream.Flush();
byte[] json = stream.ToArray();
returnValue = Encoding.UTF8.GetString(json, 0, json.Length);
}
return returnValue;
}
My complete identity class follows:
using System;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Web.Security;
namespace MvcFormsAuth.Security
{
public class CustomIdentity : ICustomIdentity
{
/// <summary>
/// Authenticate and get identity out with roles
/// </summary>
/// <param name="userName">User name</param>
/// <param name="password">Password</param>
/// <returns>Instance of identity</returns>
public static CustomIdentity GetCustomIdentity(string userName, string password)
{
CustomIdentity identity = new CustomIdentity();
if (Membership.ValidateUser(userName, password))
{
identity.IsAuthenticated = true;
identity.Name = userName;
var roles = System.Web.Security.Roles.GetRolesForUser(userName);
identity.Roles = roles;
return identity;
}
return identity;
}
private CustomIdentity() { }
public string AuthenticationType
{
get { return "Custom"; }
}
public bool IsAuthenticated { get; private set; }
public string Name { get; private set; }
private string[] Roles { get; set; }
public bool IsInRole(string role)
{
if (string.IsNullOrEmpty(role))
{
throw new ArgumentException("Role is null");
}
return Roles.Where(one => one.ToUpper().Trim() == role.ToUpper().Trim()).Any();
}
/// <summary>
/// Create serialized string for storing in a cookie
/// </summary>
/// <returns>String representation of identity</returns>
public string ToJson()
{
string returnValue = string.Empty;
IdentityRepresentation representation = new IdentityRepresentation()
{
IsAuthenticated = this.IsAuthenticated,
Name = this.Name,
Roles = string.Join("|", this.Roles)
};
DataContractJsonSerializer jsonSerializer =
new DataContractJsonSerializer(typeof(IdentityRepresentation));
using (MemoryStream stream = new MemoryStream())
{
jsonSerializer.WriteObject(stream, representation);
stream.Flush();
byte[] json = stream.ToArray();
returnValue = Encoding.UTF8.GetString(json, 0, json.Length);
}
return returnValue;
}
/// <summary>
/// Create identity from a cookie data
/// </summary>
/// <param name="cookieString">String stored in cookie, created via ToJson method</param>
/// <returns>Instance of identity</returns>
public static ICustomIdentity FromJson(string cookieString)
{
IdentityRepresentation serializedIdentity = null;
using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(cookieString)))
{
DataContractJsonSerializer jsonSerializer =
new DataContractJsonSerializer(typeof(IdentityRepresentation));
serializedIdentity = jsonSerializer.ReadObject(stream) as IdentityRepresentation;
}
CustomIdentity identity = new CustomIdentity()
{
IsAuthenticated = serializedIdentity.IsAuthenticated,
Name = serializedIdentity.Name,
Roles = serializedIdentity.Roles
.Split(new string[] { "|" }, StringSplitOptions.RemoveEmptyEntries)
};
return identity;
}
}
}
Principal class is much lighter, and it mostly responsible for setting User property on HttpContext:
using System;
using System.Security.Principal;
using System.Web;
using System.Web.Security;
namespace MvcFormsAuth.Security
{
public class CustomPrincipal : ICustomPrincipal
{
private CustomPrincipal() { }
private CustomPrincipal(ICustomIdentity identity)
{
this.Identity = identity;
}
public IIdentity Identity { get; private set; }
public bool IsInRole(string role)
{
if (string.IsNullOrEmpty(role))
{
throw new ArgumentException("Role is null");
}
return ((ICustomIdentity)Identity).IsInRole(role);
}
public static void Logout()
{
HttpContext.Current.User =
new GenericPrincipal(new GenericIdentity(""), new string[] { });
}
/// <summary>
/// Login
/// </summary>
/// <param name="userName">User name</param>
/// <param name="password">Password</param>
/// <param name="rememberMe">True, if authentication should persist between browser sessions
/// </param>
/// <returns>True if login succeeds</returns>
public static bool Login(string userName, string password, bool rememberMe)
{
var identity = CustomIdentity.GetCustomIdentity(userName, password);
if (identity.IsAuthenticated)
{
HttpContext.Current.User = new CustomPrincipal(identity);
FormsAuthenticationTicket ticket =
new FormsAuthenticationTicket(
1, identity.Name, DateTime.Now, DateTime.Now.AddMinutes(30), rememberMe,
identity.ToJson(), FormsAuthentication.FormsCookiePath);
string encryptedTicket = FormsAuthentication.Encrypt(ticket);
var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
cookie.Path = FormsAuthentication.FormsCookiePath;
if (rememberMe)
{
cookie.Expires = DateTime.Now.AddYears(1);// good for one year
}
HttpContext.Current.Response.Cookies.Add(cookie);
}
return identity.IsAuthenticated;
}
public static bool Login(string cookieString)
{
ICustomIdentity identity = CustomIdentity.FromJson(cookieString);
if (identity.IsAuthenticated)
{
HttpContext.Current.User = new CustomPrincipal(identity);
}
return identity.IsAuthenticated;
}
}
}
The most interested code is in Login methods. Primary login method delegates authentication to identity class, then just sets User on HttpContext. You may notice how “remember me” is implemented. I am doing it by setting expiration date on authentication cookie. I am making it valid for one year since login.
So far so good. Now what do we do when authenticated user hits a page? We will manually implement sliding expiration and call the secondary login method of out principal. The most logical place for that is in a base controller we are going to create by overriding OnAuthorization method:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
using MvcFormsAuth.Security;
namespace MvcFormsAuth.Controllers
{
[Authorize]
public class AuthorizedController : Controller
{
protected override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
HttpCookie cookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if (cookie != null)
{
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);
var newTicket = FormsAuthentication.RenewTicketIfOld(ticket);
if (newTicket.Expiration != ticket.Expiration)
{
string encryptedTicket = FormsAuthentication.Encrypt(newTicket);
cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
cookie.Path = FormsAuthentication.FormsCookiePath;
Response.Cookies.Add(cookie);
}
CustomPrincipal.Login(ticket.UserData);
}
}
}
}
Now all my controllers that need to be authenticated and authorized can just inherit from this base class. The login call simply uses user data I added to start with to rehydrate the identity and principal objects.
To summarizer, because I am saving some necessary data in the cooking I am saving an extra call or two to the database to get user and roles information on subsequent requests.
Here is a little bit more information on the subject that will be helpful
To enforce authentication rules on your entire site, add the following to web.config
<system.web>
<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>
<authorization>
<deny users="?"/>
</authorization>
Make sure to exclude your styles and scripts from authentication. Otherwise you will notice that your login page is not styled. To do so, just add location configuration to your web.config as follows. The same applies to your login/register, etc… methods.
<!– allow unauthenticated users to get CSS data –>
<location path="Content">
<system.web>
<authorization>
<allow users="?"/>
</authorization>
</system.web>
</location>
<location path="Account">
<system.web>
<authorization>
<allow users="?"/>
</authorization>
</system.web>
</location>
</configuration>
You can download entire sample application here.
Please share your opinions on this subject.