Tuesday, April 17, 2012

[.NET] MVC 3.0, Razor, Custom Principal and Identity

The goal here is to render user info such as the full name instead of the user login name. I'm thinking that I could've used static classes or store info in the Global file, but I wanted to avoid doing those regardless. So lets get started.

You'll need to make a custom Principal and Identity classes, where the Identity contains additional info. These can be these class files anywhere you want. Or declare it some security class file and just make the classes in there.

Principal class:

public class CustomPrincipal : IPrincipal
    {
        private CustomIdentity _Identity; 
 
        public IIdentity Identity 
        { 
            get { return _Identity; } 
        } 
          
        public CustomPrincipal(CustomIdentity identity) 
        {
            _Identity = identity;
        } 
    
        public bool IsInRole(string role) 
        { 
            throw new NotImplementedException(); 
        } 
    }

Identity class:

public class CustomIdentity : IIdentity
    {
        private string _UserName;
        private string _DisplayName;
        private bool _IsAuthenticated;
        private Roles _Role;

        public CustomIdentity(string username)
        {
 
                _UserName = username;
                _IsAuthenticated = true;
                _Role = Roles.Admin;
        }

        public string AuthenticationType
        {
            get { return "custom authentication"; }
        }

        public bool IsAuthenticated
        {
            get { return _IsAuthenticated; }
        }

        public string Name
        {
            get { return _UserName; }
        }

        public string DisplayName
        {
            get { return _DisplayName; }
            set { _DisplayName = value; }
        }

        public Roles Role
        {
            get { return _Role; }
        }
    }

    public enum Roles
    {
        Admin = 1,
        User = 2,
        Guest = 3
    }


If you noticed the Roles class, ignore that. I won't be going over it until a next update.

Now go to your Global.asax file and add this method:

protected void Application_AuthenticateRequest(Object sender, EventArgs e) {
            // Get the authentication cookie
            string cookieName = FormsAuthentication.FormsCookieName;
            HttpCookie authCookie = Context.Request.Cookies[cookieName];

            // If the cookie can't be found, don't issue the ticket
            if (authCookie == null) return;

            // Get the authentication ticket and rebuild the principal 
            // & identity
            FormsAuthenticationTicket authTicket =
              FormsAuthentication.Decrypt(authCookie.Value);
            string[] roles = authTicket.UserData.Split(new Char[] { '|' });
            CustomIdentity userIdentity =  new CustomIdentity(authTicket.Name);
            userIdentity.DisplayName = authTicket.UserData;
            CustomPrincipal userPrincipal = new CustomPrincipal(userIdentity);
            Context.User = userPrincipal;
          
    }



Being a stateless application, these classes get called when a page loads. So this method gets called to make sure the current user is authenticated. Also, notice where the userIdentity stuff is at. I used the FormsAuthenticationTicket UserData property to store a string of data. I guess if you have more than one data you want to store, you can separate the literal by commas and parse it later.

In the controller, lets say the AccountController that appears in the default templates, go to the LogOn method that has the HttpPost attribute. Here you want to do all your login comparisons, whether you are using a stored procedure of an external database or by other methods. In the end, you'll want to create your FormsAuthenticationTicket here. And here it is:

FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1, model.UserName, DateTime.Now, DateTime.Now.AddMinutes(15), false, "Phil Jackson");
                        string encTicket = FormsAuthentication.Encrypt(authTicket);
                        HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
                        Response.Cookies.Add(faCookie);

                        string redirectUrl = FormsAuthentication.GetRedirectUrl(model.UserName, false);
                        Response.Redirect(redirectUrl);


The last parameter in the FormsAuthenticationTicket constructor is the user data string. So I hardcoded the name for this example, but you can retrieve strings of data from a database and use them here.

If you run it now, everything should work, but you still cannot render the name yet. So the idea is to create an Extensions class. These classes allow you to add more fields when using Razor. For example, we want to use User.DisplayFullName instead of User.Name. DisplayFullName is not a member originally so that is where extensions come in.

So you can create a new class, which can be stored anywhere as well:


public static class PrincipalExtensions
    {
        public static CustomIdentity GetMyIdentity(this IPrincipal principal) 
        { 
            return principal.Identity as CustomIdentity; 
        } 
    }


It's pretty obvious what this method does so no explaination is needed here. But we cannot use this yet. I guess, that we have to "reference" or "add the namespace" of the class in the View's Web.config. And here it is:



<system.web.webPages.razor>
    <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <pages pageBaseType="System.Web.Mvc.WebViewPage">
      <namespaces>
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Ajax" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Routing" />
        <add namespace="IR_Project.Security"/>
      </namespaces>
    </pages>
  </system.web.webPages.razor>


Now we can use it!! Just do something like:

User.GetIdentity().DisplayName

1 comment:

  1. Awesome sample. I did run into a weird error message during the serialization process about "SerializationException: Type is not resolved for member..." but it was solved by having my custom identity object inherit from MarshalByRefObject and also putting the [Serializable] attribute on it.

    cheers,

    Ray S.

    ReplyDelete