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.
Hi Sergey,
Thanks for your efforts here… i really need to download your solution and need to look into this. I have tried couple of times and it seems your download link is not working. It would be great if you fix it please.
Cheers.
I just tested the link, and it works for me. Make sure to enable popups in your browser.
Thanks.
Article was a good and effective. And now this article, and I learned from experience with MVC 2.0 application, I decided I’d done, thanks to my blog.
Hello Sergey, congratulations on the article.
I only have one comment and one question.
1. Instead you create a controller and overwrite the OnAuthorization, you might customize the Authorize attribute? Because in this way, applications that only a few actions, have the attribute they could not authorize using this controller.
2. What’s utilizade FormsAuthentication.FormsCookiePath, I really do not quite understand this parameter, and indeed I do not use it.
Hello.
On item #1, it is valid approach, and I think it is a matter of personal preference. I like to express my intentions in base classes.
On item#2, it is simply a path to the cookie as seen by the browser. I am using it for the sake of compatibility with ASP.NET, that is all, making my code more generic. This is not required of course.
Hi Sergey!
Great artilce, but what if cookies is disabled from the browser how do you solve the authentication and authorization?
@EXE
You cannot. You have to use some other method at that point. Maybe add the same cooking data to the header of the request and have server pull it back out and verify. Of course, if you are using http client in a mobile application for example, you have full control over cookies. This only breaks down in a pure browser based app.
Good article, However i would like to manage custom roles here as well (in mine case 3 roles would be there) i am not using membership provider, How can i handle it Or how can i override isinrole method?
@Arun,
You can always store role in the user data field in the cooking and set it back at the principal after logging back in on post backs/ calls.
@Sergey I had implemented role, now in mine case i have 3 roles 1)admin 2)user and in admin i need functionality to impersonate user account and also need to keep track that any action taken by impersonating account (i.e. need both ID admin & user) what should be possible solution?
@Arun
If you are talking about impersonation in IIS, I am not sure custom auth would help you. You can certainly have info about role in the cookie, and act accordingly, but why would you need to impersonate a windows user? Seems like you would just use Windows Auth maybe?
Great article! This was very helpful for someone new to MVC. One question: my users’ sessions never seem to timeout/expire. Every time I request a resource and the base controller’s OnAuthorization gets called, “expired” tickets are simply renewed. What mechanism (if any) will redirect expired sessions to the login page?
If you return status 401 from your authentication method, you should get redirect from ASP.NET. It is also possible you have sliding expiration turned on.