Custom Identity using MVC5 and OWIN
Asked Answered
E

4

22

I trying to add custom properties to the ApplicationUser for a web site using MVC5 and OWIN authentication. I've read https://mcmap.net/q/63665/-asp-net-mvc-set-custom-iidentity-or-iprincipal and I like how it integrates with the base controller for easy access to the new properties. My issue is that when I set the HTTPContext.Current.User property to my new IPrincipal I get a null reference error:

[NullReferenceException: Object reference not set to an instance of an object.]
   System.Web.Security.UrlAuthorizationModule.OnEnter(Object source, EventArgs eventArgs) +127
   System.Web.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +136
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +69

Here is my code:

    protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
    {
        if (HttpContext.Current.User.Identity.IsAuthenticated)
        {
            userManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()));

            ApplicationUser user = userManager.FindByName(HttpContext.Current.User.Identity.Name);

            PatientPortalPrincipal newUser = new PatientPortalPrincipal();
            newUser.BirthDate = user.BirthDate;
            newUser.InvitationCode = user.InvitationCode;
            newUser.PatientNumber = user.PatientNumber;

            //Claim cPatient = new Claim(typeof(PatientPortalPrincipal).ToString(), );

            HttpContext.Current.User = newUser;
        }
    }

public class PatientPortalPrincipal : ClaimsPrincipal, IPatientPortalPrincipal
{
    public PatientPortalPrincipal(ApplicationUser user)
    {
        Identity = new GenericIdentity(user.UserName);
        BirthDate = user.BirthDate;
        InvitationCode = user.InvitationCode;
    }

    public PatientPortalPrincipal() { }

    public new bool IsInRole(string role)
    {
        if(!string.IsNullOrWhiteSpace(role))
            return Role.ToString().Equals(role);

        return false;
    }

    public new IIdentity Identity { get; private set; }
    public WindowsBuiltInRole Role { get; set; }
    public DateTime BirthDate { get; set; }
    public string InvitationCode { get; set; }
    public string PatientNumber { get; set; }
}

public interface IPatientPortalPrincipal : IPrincipal
{

    WindowsBuiltInRole Role { get; set; }
    DateTime BirthDate { get; set; }
    string InvitationCode { get; set; }
    string PatientNumber { get; set; }
}

I haven't found much in the way of documentation on how to do this, I've read these articles:

http://blogs.msdn.com/b/webdev/archive/2013/10/16/customizing-profile-information-in-asp-net-identity-in-vs-2013-templates.aspx

http://blogs.msdn.com/b/webdev/archive/2013/07/03/understanding-owin-forms-authentication-in-mvc-5.aspx

The comments in the second link pointed me to perhaps using claims (http://msdn.microsoft.com/en-us/library/ms734687.aspx?cs-save-lang=1&cs-lang=csharp) , but the article linked to doesn't show how to add those to an IPrincipal (which is what HttpContext.Current.User is), or where in the pipeline you're supposed to add them to a ClaimsIdentity (which is the concrete class the User is). I'm leaning towards using claims, but I need to know where to add these new claims to the user.

Even if claims are the way to go, I'm curious as to what I'm doing wrong with my custom IPrincipal, as I seem to have implemented everything it requires.

Enrich answered 10/2, 2014 at 14:28 Comment(5)
moved my previous update to an answer, seemed to be more appropriateEnrich
Here's an example I wrote that leverages MVC5 features like AuthenticationFilter for this purpose: https://mcmap.net/q/66053/-how-can-i-get-the-specific-fields-of-the-currently-logged-in-user-in-mvc5Feuchtwanger
Hi BlackIce. How did you get the roles to work? What is WindowsBuiltInRole? I also followed exact code as LukeP (in thread you linked) but for me when I do [Authorize(Roles="Admin")] it doesn't work.Natashianatassia
@Ciwan WindowsBuiltInRole is an enum from windows security, msdn.microsoft.com/en-us/library/…Enrich
Never mind, it turns out LukeP's answer was 'old'. On MVC5 we're suppose to use Claims. And it's true! It works nicely.Natashianatassia
E
18

I can get something to work using Claims based security, so if you're looking to get something done quickly here is what I have at the moment:

In the login process in the AccountController (mine is within SignInAsync method), add a new claim to the identity created by UserManager:

private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
    var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
    identity.AddClaim(new Claim("PatientNumber", user.PatientNumber)); //This is what I added
    AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}

Then in my base controller classes I simply added a property:

private string _patientNumber;
public string PatientNumber
{
    get
    {
        if (string.IsNullOrWhiteSpace(_patientNumber))
        {
            try
            {
                var cp = ClaimsPrincipal.Current.Identities.First();
                var patientNumber = cp.Claims.First(c => c.Type == "PatientNumber").Value;
                _patientNumber = patientNumber;
            }
            catch (Exception)
            {
            }
        }
        return _patientNumber;
    }
}

This link was helpful for claims knowledge: http://msdn.microsoft.com/en-us/library/ms734687.aspx?cs-save-lang=1&cs-lang=csharp#code-snippet-1


Update for the issue with IPrincipal

I tracked it down to the Identity property. The issue was that I was providing a default constructor on the PatientPortalPrincipal class that was not setting the Identity property. What I ended up doing was removing the default constructor and calling the correct constructor from within Application_PostAuthenticateRequest, updated code is below

protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
    if (HttpContext.Current.User.Identity.IsAuthenticated)
    {
        userManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()));

        ApplicationUser user = userManager.FindByName(HttpContext.Current.User.Identity.Name);

        PatientPortalPrincipal newUser = new PatientPortalPrincipal(user);
        newUser.BirthDate = user.BirthDate;
        newUser.InvitationCode = user.InvitationCode;
        newUser.PatientNumber = user.PatientNumber;

        //Claim cPatient = new Claim(typeof(PatientPortalPrincipal).ToString(), );

        HttpContext.Current.User = newUser;
    }
}

That makes the whole thing work!

Enrich answered 11/2, 2014 at 12:57 Comment(2)
Claims seems to be much less intrusive than the other options, so I went with this solution.Enrich
I also wasn't setting IIdentity which resulted in the above error. So I recommend double check the properties on IParticipal implementation to avoid the error.Spokane
W
5

You're getting an exception because HttpContext.Current.User.Identity.IsAuthenticated returns false at the point of check (so does HttpContext.Current.Request.IsAuthenticated).

If you remove the if (HttpContext.Current.User.Identity.IsAuthenticated) statement it will work fine (at least this part of code).

I've tried a simple thing like this:

BaseController.cs

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

CustomPrincipal.cs

public class CustomPrincipal : IPrincipal
{
    public IIdentity Identity { get; private set; }
    public bool IsInRole(string role) { return false; }

    public CustomPrincipal(string username)
    {
        this.Identity = new GenericIdentity(username);
    }

    public DateTime BirthDate { get; set; }
    public string InvitationCode { get; set; }
    public int PatientNumber { get; set; }
}

Global.asax.cs

protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
     CustomPrincipal customUser = new CustomPrincipal(User.Identity.Name);

     customUser.BirthDate = DateTime.Now;
     customUser.InvitationCode = "1234567890A";
     customUser.PatientNumber = 100;

     HttpContext.Current.User = customUser;
}

HomeController.cs

public ActionResult Index()
{
    ViewBag.BirthDate = User.BirthDate;
    ViewBag.InvitationCode = User.InvitationCode;
    ViewBag.PatientNumber = User.PatientNumber;

    return View();
}

And this is working fine. So unless this code:

userManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()));

ApplicationUser user = userManager.FindByName(HttpContext.Current.User.Identity.Name);

is not returning a valid (custom) user object, the problem is with the if() statement.

Your update looks fine, and if you're happy to store data as claims in a cookie you can go with it, although I personally hate the try {} catch block there.

What I do instead is this:

BaseController.cs

[AuthorizeEx]
public abstract partial class BaseController : Controller
{
    public IOwinContext OwinContext
    {
        get { return HttpContext.GetOwinContext(); }
    }

    public new ClaimsPrincipal User
    {
        get { return base.User as ClaimsPrincipal; }
    }

    public WorkContext WorkContext { get; set; }
}

I decorate the base controller class with a custom attribute.

AuthorizeExAttribute.cs:

public class AuthorizeExAttribute : AuthorizeAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        Ensure.Argument.NotNull(filterContext);

        base.OnAuthorization(filterContext);

        IPrincipal user = filterContext.HttpContext.User;
        if (user.Identity.IsAuthenticated)
        {
            var ctrl = filterContext.Controller as BaseController;
            ctrl.WorkContext = new WorkContext(user.Identity.Name);
        }
    }
}

And WorkContext.cs:

public class WorkContext
{
    private string _email;

    private Lazy<User> currentUser;

    private IAuthenticationService authService;
    private ICacheManager cacheManager;

    public User CurrentUser
    {
        get 
        { 
            var cachedUser = cacheManager.Get<User>(Constants.CacheUserKeyPrefix + this._email);
            if (cachedUser != null)
            {
                return cachedUser;
            }
            else
            {
                var user = currentUser.Value;

                cacheManager.Set(Constants.CacheUserKeyPrefix + this._email, user, 30);

                return user;
            }
        }
    }

    public WorkContext(string email)
    {
        Ensure.Argument.NotNullOrEmpty(email);

        this._email = email;

        this.authService = DependencyResolver.Current.GetService<IAuthenticationService>();
        this.cacheManager = DependencyResolver.Current.GetService<ICacheManager>();

        this.currentUser = new Lazy<User>(() => authService.GetUserByEmail(email));
    }

I then access the WorkContext like this:

public class DashboardController : BaseController
{
    public ActionResult Index()
    {
        ViewBag.User = WorkContext.CurrentUser;

        return View();
    }
}

I'm using Ninject's Dependency Resolver to resolve authService and cacheManager but you can skip caching and replace authService with ASP.NET Identity UserManager I believe.

I also wanted to give credit where it's due as the WorkContext class is heavily inspired by NugetGallery project.

Weinhardt answered 10/2, 2014 at 21:58 Comment(3)
that's not the reason for my error, I only get the exception when User gets replaced, which only happens if IsAuthenticated is true.Enrich
@Enrich In my testing scenario it seems that Application_PostAuthenticateRequest is firing twice on every request and if I put a breakpoint on your if clause, the first time it runs HttpContext.Current.User (or Identity) is null and I get a null exception on the clause itself. Can you place a breakpoint on that if clause and see if Identity is null when it fires? If it is, than IsAuthenticated will throw NullReferenceException.Weinhardt
Hi Luke - hopefully you see this as it's an old post. Anyway, I can't get the WorkContext.cs to work. Check out gist.github.com/ajtatum/5d06a2dbcc6d1172efae609720b6a095. Id did really like your solution here: stackoverflow.com/a/10524305 as it's much easier to access the properties. I would like it to get it working with OWIN, mainly because it handles external logins/registration through Google and such. I've created a question here: #39680753 if you're give your input that would be great! Thank you! -AJDevaluate
A
3

I bet HttpContext.Current.User is null. So instead of this:

if (HttpContext.Current.User.Identity.IsAuthenticated)

you can try this:

if (HttpContext.Current.Request.IsAuthenticated)
Antoine answered 10/2, 2014 at 21:23 Comment(1)
No, it successfully made it through my method, it errors out somewhere in the MVC code.Enrich
A
0

I've had the same error.

My problem was that with anonymous users I wasn't setting the IIdentity on IPrincipal. I did this only when users logged in with user name. Otherwise, IIdentity was null.

My solution was to always set IIdentity. If user is not authenticated (anonymous user) then IIdentity.IsAuthenticated is set to false. Otherwise, true.

My code:

private PrincipalCustom SetPrincipalIPAndBrowser()
{
     return new PrincipalCustom
     {
       IP = RequestHelper.GetIPFromCurrentRequest(HttpContext.Current.Request),
       Browser = RequestHelper.GetBrowserFromCurrentRequest(HttpContext.Current.Request),

    /* User is not authenticated, but Identity must be set anyway. If not, error occurs */
       Identity = new IdentityCustom { IsAuthenticated = false }
     };
}
Abbe answered 30/9, 2016 at 9:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.