Revisiting FormsAuthentication.

FormsAuthentication module exists as an in-built HttpModule. For every incoming request, if it finds a valid, non-expired ‘FormsAuthenticationTicket’, it will try to authenticate the user and create a GeneralPrincipal object. The ‘GeneralPrincipal’ object represents the current user. Below are some examples, highlighting how to work with, additional info attached to the AuthenticationTicket.

Scenario 1:  A simple Forms Authentication example
web.config

<authentication mode="Forms">
            <forms loginUrl="SignOn.aspx" name="AuthCookie" timeout="60" path="/">
            </forms>
        </authentication>
        <authorization>
            <deny users="?"/>
            <allow users="*"/>
        </authorization>

SignOn.aspx.cs

protected void btnLogin_Click(object sender, EventArgs e)
{
bool isAuthenticated = IsAuthenticated(txtUserName.Text, txtPassword.Text);
if (isAuthenticated == true)
{
    // generate authentication ticket
    FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1,   // version
                            txtUserName.Text,           // username
                            DateTime.Now,               // creation
                            DateTime.Now.AddMinutes(60),// Expiration
                            false,   // Persistent
                            ""   //No additional data supplied
                        );                     
    // Encrypt the ticket.
    string encryptedTicket = FormsAuthentication.Encrypt(authTicket);
    // Create a cookie and add the encrypted ticket to the cookie
    HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName,
                                encryptedTicket); 
    Response.Cookies.Add(authCookie);
    // Redirect the user to the originally requested page
    Response.Redirect(FormsAuthentication.GetRedirectUrl(
                                                    txtUserName.Text,
                                                    false));
}
}

private bool IsAuthenticated(string username, string password)
{
    //The user can be validated/authenticated against custom SQl Server database, 
    //or membership or ActiveDirectory users.
    // Need to implement your own authentication logic. 
    return true;
}

Finally my landing page ‘SecurePage0.aspx ‘.

Few things to notice here. After user is successfully authenticated, the in-built FormsAuthentication module attaches the GenericPrincipal object to the Context.User. I don’t have to do that.The GenericPrincipal object contains an Identity object and roles info. This object is now available throughout the current request, something like a request basis cache. We’ll look into that in more details, a little later.

using System.Security.Principal;

public partial class SecurePage0 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        GenericPrincipal cp1 = (GenericPrincipal)HttpContext.Current.User;
        bool _isAuthenticated = Context.User.Identity.IsAuthenticated;
        string _userName = Context.User.Identity.Name;
        bool _role = Context.User.IsInRole("Manager");
    }
}

Here the GenericPrincipal as well as Context.User contains logged-in user’s username,  whether authenticated or not and absolutely NO role information. Remember, while creating the AuthenticationTicket, I passed an empty string for user data argument.(hence IsInRole function always returns false). Exact same information is available in Context.User too. So in this default mode , I have 2 pieces of user-information available – IsAuthenticated and UserName – in the context object as well as in my GenericPrincipal which anyway represents the current logged-in user.The in-built FormsAuthentication module did all the work.

img1

Scenario 2: Forms Authentication with Roles

Now let’s say, I want to add some additional information about roles, the current user belongs in. The GenericPrincipal object contains roles information fields.Hence when I create the FormsAuthentication ticket manually, I can attach the roles for the given user, as a string object to the ticket Here’s the FormsAuthenticationTicket creation code,which adds role info as the last parameter.

protected void btnLogin_Click(object sender, EventArgs e)
{
bool isAuthenticated = IsAuthenticated(txtUserName.Text, txtPassword.Text);
if (isAuthenticated == true)
{
    // generate authentication ticket
    FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1,   // version
                            txtUserName.Text,           // username
                            DateTime.Now,               // creation
                            DateTime.Now.AddMinutes(60),// Expiration
                            false,   // Persistent
                            GetRoles(txtUserName.Text)   //Additional role info
                        );                     
    // Encrypt the ticket.
    string encryptedTicket = FormsAuthentication.Encrypt(authTicket);
    // Create a cookie and add the encrypted ticket to the cookie
    HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName,
                                encryptedTicket); 
    Response.Cookies.Add(authCookie);
    // Redirect the user to the originally requested page
    Response.Redirect(FormsAuthentication.GetRedirectUrl(
                                                    txtUserName.Text,
                                                    false));
}
}

private bool IsAuthenticated(string username, string password)
{
    //The user can be validated/authenticated against custom SQl Server database, 
    //or membership or ActiveDirectory users.
    // Need to implement your own authentication logic. 
    return true;
}
private string GetRoles(string username)
{
    //return dummy role string. In real scenario, get roles from 
    //appropriate source like DB.
    return "Senior Manager|Manager|Employee";
}

The default FormsAuthentication module won’t attach role info to the GenericPrincipal object. I need to write code for that. Global.asax —> Application_PostAuthenticateRequest event handler is the spot, wherein I can attach roles to the Principal object. So here’s my global.asax

protected void Application_PostAuthenticateRequest(object sender, EventArgs e)
{
string cookieName = FormsAuthentication.FormsCookieName;
HttpCookie authCookie = Context.Request.Cookies[cookieName];

if (null == authCookie)
{
    // No authentication cookie.
    return;
}

//4.Add the following code to extract and decrypt the authentication 
//ticket from the forms authentication cookie. 
FormsAuthenticationTicket authTicket = null;
try
{
    authTicket = FormsAuthentication.Decrypt(authCookie.Value);
}
catch (Exception ex)
{
    // Log exception details 
    return;
}

if (null == authTicket)
{
    // Cookie failed to decrypt.
    return;
}
string[] roles = authTicket.UserData.Split('|');
FormsIdentity id = new FormsIdentity(authTicket);
HttpContext.Current.User = new System.Security.Principal.GenericPrincipal(id, roles);
//There are 2 security contexts - Thread.CurrentPrincipal and Context.User
//need to keep thread.CurrentPrincipal in sync with Context.User
//Thread.Currentprincipal is used in declarative role checks
//using PrincipalPermissionAttributes
System.Threading.Thread.CurrentPrincipal = HttpContext.Current.User;
}

Now in one of my authenticated page I can check roles, something like this

using System.Security.Principal;

public partial class SecurePage02 : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
    GenericPrincipal cp = (GenericPrincipal)HttpContext.Current.User;
    bool _isAuthenticated = Context.User.Identity.IsAuthenticated;
    string _userName = Context.User.Identity.Name;
    bool _role = Context.User.IsInRole("Manager");
        
    Response.Write("Authenticated Identity is: " + cp.Identity.Name);
    Response.Write("<p>");

    if (cp.IsInRole("Senior Manager"))
    {
        Response.Write(cp.Identity.Name + " is in the " + "Senior Manager Role");
        Response.Write("<p>");
    }
}
}

Since my Thread.CurrentPrincipal is updated, I can even write role compliant class or function.

[PrincipalPermission(SecurityAction.Demand, Role="Manager")]
    public class Class1
    {
        public string str1 = "xxx";
    }

    [PrincipalPermission(SecurityAction.Demand, Role="Manager")]
    public void abc1()
    {
        //do something
    }

If any user , who does not belong to the role ‘Manager’, tries to access the class, a SecurityException is raised.

Scenario 3: CustomPrincipal.  The GenericPrincipal class has only one Role related member function “IsInRole”. What if I want to check, if user belongs to a set of roles or things like IsUserPresentInAnyRole? basically extending Role functionality, — then I need a CustomPrincipal class which implements IPrincipal interface.Here’s an example.

//CustomPrincipal class

public class GCustomPrincipal : IPrincipal
{
    private IIdentity _identity;
    private string[] _roles;

    // IPrincipal Implementation
    public bool IsInRole(string role)
    {
        return Array.BinarySearch(_roles, role) >= 0 ? true : false;
    }
    public IIdentity Identity
    {
        get
        {
            return _identity;
        }
    }

    public GCustomPrincipal(IIdentity identity, string [] roles)
    {
      _identity = identity;
      _roles = new string[roles.Length];
      roles.CopyTo(_roles, 0);
      Array.Sort(_roles);
    }

    //Add the following two public methods, which provide extended role-based checking functionality.
    // Checks whether a principal is in all of the specified set of roles
    public bool IsInAllRoles( params string [] roles1 )
    {
      foreach (string searchrole in _roles )
      {
        if (Array.BinarySearch(_roles, searchrole) < 0 )
          return false;
      }
      return true;
    }

    // Checks whether a principal is in any of the specified set of roles
    public bool IsInAnyRoles( params string [] roles1 )
    {
      foreach (string searchrole in _roles )
      {
        if (Array.BinarySearch(roles1, searchrole ) > 0 )
          return true;
      }
      return false;
    }
}

Global.asax

protected void Application_PostAuthenticateRequest(object sender, EventArgs e)
    {
        string cookieName = FormsAuthentication.FormsCookieName;
        HttpCookie authCookie = Context.Request.Cookies[cookieName];

        if (null == authCookie)
        {
            // There is no authentication cookie.
            return;
        }

        //Add the following code to extract and decrypt the 
        //authentication ticket from the forms authentication cookie. 
        FormsAuthenticationTicket authTicket = null;
        try
        {
            authTicket = FormsAuthentication.Decrypt(authCookie.Value);
        }
        catch (Exception ex)
        {
            return;
        }

        if (null == authTicket)
        {
            // Cookie failed to decrypt.
            return;
        }

        //Add the following code to parse out the pipe separate list of role names attached to the 
        //ticket when the user was originally authenticated. 
        // When the ticket was created, the UserData property was assigned a
        // pipe delimited string of role names.
        string[] roles = authTicket.UserData.Split('|');

        //Add the following code to create a FormsIdentity object 
        //with the user name obtained from the ticket name and 
        //a CustomPrincipal object that contains this identity together with the user's role list. 
        FormsIdentity id = new FormsIdentity(authTicket);

        // This principal will flow throughout the request.
        GCustomPrincipal principal = new GCustomPrincipal(id, roles);
        // Attach the new principal object to the current HttpContext object
        Context.User = principal;
        //need to keep thread.CurrentPrincipal in sync with Context.User
        //Thread.Currentprincipal is used in declarative role checks
        //using PrincipalPermissionAttributes
        System.Threading.Thread.CurrentPrincipal = HttpContext.Current.User;
    }

Authenticated page

protected void Page_Load(object sender, EventArgs e)
{
    GCustomPrincipal cp = HttpContext.Current.User as GCustomPrincipal;
    Response.Write("Authenticated Identity is: " + cp.Identity.Name);
    Response.Write("<p>");

    if ( cp.IsInRole("Senior Manager") )
    {
        Response.Write( cp.Identity.Name + " is in the " + "Senior Manager Role" );
        Response.Write( "<p>" );            
    }

    if ( cp.IsInAnyRoles("Senior Manager", "Manager", "Employee", "Sales") )
    {
        Response.Write( cp.Identity.Name + " is in one of the specified roles");
        Response.Write( "<p>" );
    }
}

Scenario 4: CustomPrincipal and CustomIdentity

Sometimes I may want to save more user info, say things like — UserID, userCreationDate, primary email etc, in the FormsAuthenticationTicket. The ticket is safe, since it’s encrypted , signed by ASP.net. Here I need a CustomIdentity class implementing IIdentity interface. Below is my FormsAuthenticationTicket creation code. SignOn.aspx (Notice the emailid in the GetRoles function)

protected void btnLogin_Click(object sender, EventArgs e)
{
bool isAuthenticated = IsAuthenticated(txtUserName.Text, txtPassword.Text);
if (isAuthenticated == true)
{
    // generate authentication ticket
    FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1,   // version
                            txtUserName.Text,           // username
                            DateTime.Now,               // creation
                            DateTime.Now.AddMinutes(60),// Expiration
                            false,   // Persistent
                            GetRoles(txtUserName.Text)   //Additional role info
                        );                     
    // Encrypt the ticket.
    string encryptedTicket = FormsAuthentication.Encrypt(authTicket);
    // Create a cookie and add the encrypted ticket to the cookie
    HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName,
                                encryptedTicket); 
    Response.Cookies.Add(authCookie);
    // Redirect the user to the originally requested page
    Response.Redirect(FormsAuthentication.GetRedirectUrl(
                                                    txtUserName.Text,
                                                    false));
}
}

private bool IsAuthenticated(string username, string password)
{
    //The user can be validated/authenticated against custom SQl Server database, 
    //or membership or ActiveDirectory users.
    // Need to implement your own authentication logic. 
    return true;
}
private string GetRoles(string username)
{
    //return dummy role string. In real scenario, get roles from 
    //appropriate source like DB. Additional user info like emailid
    string _strEmailRoles = "a@a.com|Senior Manager,Manager,Employee";
    return _strEmailRoles;
}

CustomIdentity class

public class GCustomIdentity : IIdentity
{
    private System.Web.Security.FormsAuthenticationTicket ticket;

    public GCustomIdentity(System.Web.Security.FormsAuthenticationTicket ticket)
    {
        this.ticket = ticket;
    }

    public string AuthenticationType
    {
        get { return "FormsAuth With Custom Identity"; }
    }

    public bool IsAuthenticated
    {
        get { return true; }
    }

    public string Name
    {
        get { return ticket.Name; }
    }

    public string Email
    {
        get
        {
        string[] data = ticket.UserData.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
        if (data.Length > 0)
            return data[0];
        else
            return "";
        }
    }

    public string[] Roles
    {
        get
        {
            string[] data = ticket.UserData.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
            if (data.Length > 1)
            {
                string[] roles = data[1].Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                return roles;
            }
            else
                return new string[] { "" };
        }
    }

}

CustomPrincipal class, having CustomIdentity

public class GCustomPrincipal2 : IPrincipal
{
    private GCustomIdentity _identity;
    private string[] _roles;
    public bool IsInRole(string role)
    {
        return Array.BinarySearch(_roles, role) >= 0 ? true : false;
    }
    public IIdentity Identity
    {
        get
        {
            return this._identity;
        }
        set
        {
            this._identity = (GCustomIdentity)value;
        }
    }

    public GCustomPrincipal2(IIdentity identity, string[] roles)
    {
        this._identity = (GCustomIdentity)identity;
        _roles = new string[roles.Length];
        roles.CopyTo(_roles, 0);
        Array.Sort(_roles);
    }

    // Checks whether a principal is in any of the specified set of roles
    public bool IsInAnyRoles(params string[] roles1)
    {
        foreach (string searchrole in _roles)
        {
            if (Array.BinarySearch(roles1, searchrole) > 0)
                return true;
        }
        return false;
    }
}

Global.asax –> PostAuthenticateRequest

GCustomIdentity id = new GCustomIdentity(authTicket);
string[] roles = id.Roles;
GCustomPrincipal2 principal = new GCustomPrincipal2(id, roles);
Context.User = principal;
System.Threading.Thread.CurrentPrincipal = HttpContext.Current.User;

Few other useful facts.

1) After encryption the maximum FormsAuthentication cookie size is 4096 bytes. If the size is exceeded, the cookie won’t be send to browser.

2) FormsAuthenticationTicket info can’t be updated. For cases, when certain user profile data has changed or role info has changed just re-create the ticket and add it as a value to the same Authentication cookie.

3) The FormsAuthenticationTicket is encrypted and digitally signed before it is added to the cookie and sent to browser. On the server,just the ticket is verified and not the cookie.

4) With non-persistent cookie, if the ticket has expired and not the cookie, FormsAuthentication will still redirect user to login page. The user needs to login/authenticate again.

5) A user is authenticated only once per request. Hence during login,after successful authentication, Response.Redirect makes a brand new request and the request comes in as authenticated.

That’s all for now. Thanks for reading.

Advertisements
This entry was posted in General ASP.Net C#. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s