Custom Identity using MVC5 and OWIN

2019-03-08 04:28发布

问题:

I trying to add custom properties to the ApplicationUser for a web site using MVC5 and OWIN authentication. I've read https://stackoverflow.com/a/10524305/264607 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.

回答1:

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!



回答2:

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.



回答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)


回答4:

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 }
     };
}