ASP.NET MVC Custom Authentication

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.

13 Comments

  1. 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.

  2. 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.

  3. @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.

  4. @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?

  5. 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?

Leave a Reply

Your email address will not be published. Required fields are marked *