My Technical Notes

Monday, 22 July 2013

ASP.NET Authentication Pattern

When I write intranet web applications, I always need to set up some sort of authentication and also roles-based permissions. The first class I add is this simple class ('AppSecurity') which enables me to specify a role when putting the the authentication cookie on the user's computer. This stores the data in the authentication cookie's UserData section, which is a string.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;

//namespace MyNamespace
//{
public static class AppSecurity
{
    public static void LogOutUser(HttpContext context)
    {
        HttpContext.Current.Session.Clear();
        HttpContext.Current.Session.Abandon();
        FormsAuthentication.SignOut();
    }

    public static void SetAuthCookie(string username, string role)
    {
        HttpContext.Current.Session.Clear();
        
        // Create a new ticket used for authentication
        FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
           1, // Ticket version
           username, // Username associated with ticket
           DateTime.Now, // Date/time issued
           DateTime.Now.Add(FormsAuthentication.Timeout), // Date/time to expire
           false, // "false" for a persistent user cookie
           role, // User-data, in this case the roles
           FormsAuthentication.FormsCookiePath);

        string hash = FormsAuthentication.Encrypt(ticket);
        HttpCookie cookie = new HttpCookie(
           FormsAuthentication.FormsCookieName,
           hash);

        // Add the cookie to the list for outgoing response
        System.Web.HttpContext.Current.Response.Cookies.Add(cookie);
    }
}
//}

So when logging a user in we should call:


AppSecurity.SetAuthCookie(username, role);

The above code sets the authentication cookie, but thereafter we need to make sure that our web.config authorization elements are respected. A typical element would be like so:


<location path="AdminFolder"><!-- just for illustration, would normally call folder 'Admin' -->
  <system.web>
    <authorization>
      <allow roles="Admin" />
      <deny users="*"/>
    </authorization>
  </system.web>
</location>

The above rule should only allow "Admin" users to enter into the "AdminFolder" area. Even though we have placed an authentication cookie, we also need to add code which sets the HttpContext.Current.User so that we provide role information to ASP.NET so that it can prevent/allow access to the "AdminFolder" based on the role:


protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
    if (HttpContext.Current.User != null && HttpContext.Current.User.Identity.IsAuthenticated)
    {
        if (HttpContext.Current.User.Identity is FormsIdentity)
        {
            FormsIdentity id = (FormsIdentity)HttpContext.Current.User.Identity;
            string role = id.Ticket.UserData;

            HttpContext.Current.User = new GenericPrincipal(id, new string[] { role });
        }
    }
}

We have accomplished two things so far: 1. storing the user's role in the UserData section of the authentication cookie and 2. on each HttpRequest, reading the UserData (which now stores the role for a logged in user) and then setting the HttpContext.Current.User to include this role. This is sufficient for most people's purposes but then in some cases we also want to include data about the user such as their real name. This is useful because we can display their name on a page to make the experience of using the site more personalised. It is generally not recommended to store other data in the UserData because it will increase the size of the authentication cookie. Instead it is considered better practice to such information in the Session as this is stored server side rather than client side.

Storing data about the user in the session involves creating a class which inherits from the IIdentity interface and provides properties for storing the DisplayName (user's forename and surname combined) and other required properties. Thereafter we need to assign to HttpContext.Current.User, an IPrinciple which has as its IIdentity, an instance of our subclass.

Below we first define our IIdentity subclass:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Security.Principal;

//namespace MyNameSpace
//{
    public class AppIdentity : IIdentity
    {
        public string Name { get; set; }
        public string DisplayName { get; set; }

        public string AuthenticationType
        {
            get { return "AppIdentity"; }
        }

        public bool IsAuthenticated
        {
            get { return true; }
        }
    }
//}

Next, we need to "assign" an instance of this class to HttpContext.Current.User. Instead of retrieving the DisplayName of a user on each page load (because it will rarely change) from our backend database, it might be better to store and retrieve from the Session. (This point is debatable because in some set-ups, the Session is stored in a SQL Server Database too.) Since we are dependent on the Session being loaded, the "assignment" to HttpContext.Current.User would therefore need to take place in the Application_AcquireRequestState method in the Global class:


protected void Application_AcquireRequestState(Object sender, EventArgs e)
{
    if (HttpContext.Current.Session != null && Request.IsAuthenticated)
    {
        FormsIdentity id = (FormsIdentity)HttpContext.Current.User.Identity;

        var role = id.Ticket.UserData;
        
  AppIdentity identity;
        if (Session["AppIdentitySessionKey"] != null)
        {
            identity = Session["AppIdentitySessionKey"] as AppIdentity;
        }
        else
        {
   // the following call would be to the database to retrieve the values of the user.
            var dbUser = new { Forename = "Tahir", Surname = "Hassan" };

            identity = new AppIdentity
            {
                DisplayName = dbUser.Forename + " " + dbUser.Surname,
                Name = dbUser.userName,
                Role = role
            };

            Session["AppIdentitySessionKey"] = identity;
        }

        HttpContext.Current.User = new GenericPrincipal(identity, new string[] { role });
    }
}

Now we have three things: 1. Storing the Role in the UserData. 2. reading back the data in the Global.asax class. 3. Setting the IIdentity of the HttpContext.Current.User to be that of a custom class which stores the DisplayName.

No comments: