asp.netasp.net-mvcforms-authenticationiprincipaliidentity

ASP.NET MVC - Set custom IIdentity or IPrincipal


I need to do something fairly simple: in my ASP.NET MVC application, I want to set a custom IIdentity / IPrincipal. Whichever is easier / more suitable. I want to extend the default so that I can call something like User.Identity.Id and User.Identity.Role. Nothing fancy, just some extra properties.

I've read tons of articles and questions but I feel like I'm making it harder than it actually is. I thought it would be easy. If a user logs on, I want to set a custom IIdentity. So I thought, I will implement Application_PostAuthenticateRequest in my global.asax. However, that is called on every request, and I don't want to do a call to the database on every request which would request all the data from the database and put in a custom IPrincipal object. That also seems very unnecessary, slow, and in the wrong place (doing database calls there) but I could be wrong. Or where else would that data come from?

So I thought, whenever a user logs in, I can add some necessary variables in my session, which I add to the custom IIdentity in the Application_PostAuthenticateRequest event handler. However, my Context.Session is null there, so that is also not the way to go.

I've been working on this for a day now and I feel I'm missing something. This shouldn't be too hard to do, right? I'm also a bit confused by all the (semi)related stuff that comes with this. MembershipProvider, MembershipUser, RoleProvider, ProfileProvider, IPrincipal, IIdentity, FormsAuthentication.... Am I the only one who finds all this very confusing?

If someone could tell me a simple, elegant, and efficient solution to store some extra data on a IIdentity without all the extra fuzz.. that would be great! I know there are similar questions on SO but if the answer I need is in there, I must've overlooked.


Solution

  • Here's how I do it.

    I decided to use IPrincipal instead of IIdentity because it means I don't have to implement both IIdentity and IPrincipal.

    1. Create the interface

      interface ICustomPrincipal : IPrincipal
      {
          int Id { get; set; }
          string FirstName { get; set; }
          string LastName { get; set; }
      }
      
    2. CustomPrincipal

      public class CustomPrincipal : ICustomPrincipal
      {
          public IIdentity Identity { get; private set; }
          public bool IsInRole(string role) { return false; }
      
          public CustomPrincipal(string email)
          {
              this.Identity = new GenericIdentity(email);
          }
      
          public int Id { get; set; }
          public string FirstName { get; set; }
          public string LastName { get; set; }
      }
      
    3. CustomPrincipalSerializeModel - for serializing custom information into userdata field in FormsAuthenticationTicket object.

      public class CustomPrincipalSerializeModel
      {
          public int Id { get; set; }
          public string FirstName { get; set; }
          public string LastName { get; set; }
      }
      
    4. LogIn method - setting up a cookie with custom information

      if (Membership.ValidateUser(viewModel.Email, viewModel.Password))
      {
          var user = userRepository.Users.Where(u => u.Email == viewModel.Email).First();
      
          CustomPrincipalSerializeModel serializeModel = new CustomPrincipalSerializeModel();
          serializeModel.Id = user.Id;
          serializeModel.FirstName = user.FirstName;
          serializeModel.LastName = user.LastName;
      
          JavaScriptSerializer serializer = new JavaScriptSerializer();
      
          string userData = serializer.Serialize(serializeModel);
      
          FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(
                   1,
                   viewModel.Email,
                   DateTime.Now,
                   DateTime.Now.AddMinutes(15),
                   false,
                   userData);
      
          string encTicket = FormsAuthentication.Encrypt(authTicket);
          HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
          Response.Cookies.Add(faCookie);
      
          return RedirectToAction("Index", "Home");
      }
      
    5. Global.asax.cs - Reading cookie and replacing HttpContext.User object, this is done by overriding PostAuthenticateRequest

      protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
      {
          HttpCookie authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
      
          if (authCookie != null)
          {
              FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
      
              JavaScriptSerializer serializer = new JavaScriptSerializer();
      
              CustomPrincipalSerializeModel serializeModel = serializer.Deserialize<CustomPrincipalSerializeModel>(authTicket.UserData);
      
              CustomPrincipal newUser = new CustomPrincipal(authTicket.Name);
              newUser.Id = serializeModel.Id;
              newUser.FirstName = serializeModel.FirstName;
              newUser.LastName = serializeModel.LastName;
      
              HttpContext.Current.User = newUser;
          }
      }
      
    6. Access in Razor views

      @((User as CustomPrincipal).Id)
      @((User as CustomPrincipal).FirstName)
      @((User as CustomPrincipal).LastName)
      

    and in code:

        (User as CustomPrincipal).Id
        (User as CustomPrincipal).FirstName
        (User as CustomPrincipal).LastName
    

    I think the code is self-explanatory. If it isn't, let me know.

    Additionally to make the access even easier you can create a base controller and override the returned User object (HttpContext.User):

    public class BaseController : Controller
    {
        protected virtual new CustomPrincipal User
        {
            get { return HttpContext.User as CustomPrincipal; }
        }
    }
    

    and then, for each controller:

    public class AccountController : BaseController
    {
        // ...
    }
    

    which will allow you to access custom fields in code like this:

    User.Id
    User.FirstName
    User.LastName
    

    But this will not work inside views. For that you would need to create a custom WebViewPage implementation:

    public abstract class BaseViewPage : WebViewPage
    {
        public virtual new CustomPrincipal User
        {
            get { return base.User as CustomPrincipal; }
        }
    }
    
    public abstract class BaseViewPage<TModel> : WebViewPage<TModel>
    {
        public virtual new CustomPrincipal User
        {
            get { return base.User as CustomPrincipal; }
        }
    }
    

    Make it a default page type in Views/web.config:

    <pages pageBaseType="Your.Namespace.BaseViewPage">
      <namespaces>
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Ajax" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Routing" />
      </namespaces>
    </pages>
    

    and in views, you can access it like this:

    @User.FirstName
    @User.LastName