12

I am building a intranet application using MVC3 with a MSSQL backend. I have authentication and roles (through a custom roles provider) working properly. What I am trying to do now is overriding User.Identity to allow for items like User.Identity.FirstName. But I cannot find any code that will show me how do this in WindowsIdentity

I have tried writing a custom provider:

public class CPrincipal : WindowsPrincipal
{
    UserDAL userDAL = new UserDAL();
    public CPrincipal(WindowsIdentity identity)
        : base(identity)
    {
        userInfo = userDAL.GetUserProfile(identity.Name.Split('\\')[1]);
        this.identity = identity;
    }
    public UserInfo userInfo { get; private set; }
    public WindowsIdentity identity { get; private set; }
}

and overriding the WindowsAuthentication to populate the custom principal.

    void WindowsAuthentication_OnAuthenticate(object sender, WindowsAuthenticationEventArgs e)
    {
        if (e.Identity != null && e.Identity.IsAuthenticated)
        {
            CPrincipal cPrincipal = new CPrincipal(e.Identity);
            HttpContext.Current.User = cPrincipal;
        }
    }

I have a breakpoint in the authentication function and the principal is being populated; however, when I put a breakpoint in the controllers, the User is just its normal RolePrincipal, instead of my custom principal. What am I doing wrong?

EDIT:

I commented out the code above in the global.asax. I have overridden the AuthorizeAttribute using C#:

public class CAuthorize : AuthorizeAttribute
{
    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        bool authorized = base.AuthorizeCore(httpContext);
        if (!authorized)
        {
            return false;
        }


        IIdentity user = httpContext.User.Identity;
        CPrincipal cPrincipal = new CPrincipal(user);
        httpContext.User = cPrincipal;

        return true;
    } 

}

And adjusted my principal to the following:

public class CPrincipal : IPrincipal
{
    private UserDAL userDAL = new UserDAL();
    public CPrincipal(IIdentity identity)
    {
        userInfo = userDAL.GetUserProfile(identity.Name.Split('\\')[1]);
        this.Identity = identity;
    }
    public UserInfo userInfo { get; private set; }

    public IIdentity Identity { get; private set; }

    public bool IsInRole(string role)
    {
        throw new NotImplementedException();
    }
}

Now I when I put a breakpoint in, the watch shows the following in user:

  • User
    • [CSupport.Model.CPrincipal]
    • Identity

Identity is accessable; however, it is still the WindowsIdentity CPrincipal is only accessible in the watch and not accessible directly.

EDIT: Thanks to everyone who contributed to this. You have greatly expanded my understanding of how the various parts work.

I got both ways to work, so I thought I would share.

Option 1: Override the Authorize Request in Global.asax

This is the one I am going with.

I did not use Application_AuthenticateRequest because (according to this: HttpContext.Current.User is null even though Windows Authentication is on) the user has not been populated in a Windows authentication process and thus there is nothing that I can use to go get the user information.

Application_AuthorizeRequest is the next in the chain and happens after the windows identity is brought in.

    protected void Application_AuthorizeRequest(object sender, EventArgs e)
    {
        if (User.Identity.IsAuthenticated && Roles.Enabled)
        {
            Context.User = new FBPrincipal(HttpContext.Current.User.Identity);
        }
    }

This is the override of the Principal

public class CPrincipal : IPrincipal
{
    private UserDAL userDAL = new UserDAL();
    public CPrincipal(IIdentity identity)
    {
        userInfo = userDAL.GetUserProfile(identity.Name.Split('\\')[1]);
        this.Identity = identity;
    }
    public UserInfo userInfo { get; private set; }

    public IIdentity Identity { get; private set; }

    public bool IsInRole(string role)
    {
        return userDAL.IsUserInRole(userInfo.UserName, role);
    }
}

This is how you access the updated info in the new Principal that was created.

    [Authorize(Roles = "super admin")]
    public ActionResult Dashboard()
    {
        string firstname = (User as CPrincipal).userInfo.FirstName; // <--
        DashboardModel dModel = reportDAL.GetChartData();
        return View(dModel);
    }

Option 2: Override the AuthorizeAttribute

This is the overridden Principal (It is the same as above)

public class CPrincipal : IPrincipal
{
    private UserDAL userDAL = new UserDAL();
    public CPrincipal(IIdentity identity)
    {
        userInfo = userDAL.GetUserProfile(identity.Name.Split('\\')[1]);
        this.Identity = identity;
    }
    public UserInfo userInfo { get; private set; }

    public IIdentity Identity { get; private set; }

    public bool IsInRole(string role)
    {
        return userDAL.IsUserInRole(userInfo.UserName, role);
    }
}

Here is the override of the Authorize Attribute

public class CAuthorize : AuthorizeAttribute
{
    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        bool authorized = base.AuthorizeCore(httpContext);
        if (!authorized)
        {
            return false;
        }


        IIdentity user = httpContext.User.Identity;
        CPrincipal cPrincipal = new CPrincipal(user);
        httpContext.User = cPrincipal;

        return true;
    } 

}

This is where you change which AuthorizeAttribute to use and utilizing the new information.

    [CAuthorize(Roles = "super admin")] // <--
    public ActionResult Dashboard()
    {
        string firstname = (User as CPrincipal).userInfo.FirstName; // <--
        DashboardModel dModel = reportDAL.GetChartData();
        return View(dModel);
    }

Option 1 handles everthing globally, option 2 handles everything at an individual level.

Community
  • 1
  • 1
Toby Jones
  • 353
  • 2
  • 5
  • 13
  • Wait... What is that event handler? You're not trying to use the ASP.NET login control are you? Where is this event located and which event is it tying into? – Erik Funkenbusch Sep 24 '12 at 16:29
  • I am using windows authentication on an intranet site. This event handler is in the global.asax – Toby Jones Sep 24 '12 at 16:46
  • There is no OnAuthenticate handler in global.asax. That's probably why you are having problems. – Erik Funkenbusch Sep 24 '12 at 16:49
  • You have to cast it to a WindowsIdentity when you use it. Or, use the extension method i made below. – Erik Funkenbusch Sep 24 '12 at 16:50
  • If I cast the Identity as a WindowsIdentity in CPrincipal, it gives the following error: 'CSupport.Models.CPrincipal.Identity' cannot implement 'System.Security.Principal.IPrincipal.Identity' because it does not have the matching return type of 'System.Security.Principal.IIdentity' If I change the override to WindowsPrincipal, I get this error: 'System.Security.Principal.WindowsPrincipal' does not contain a constructor that takes 0 arguments – Toby Jones Sep 24 '12 at 16:58
  • No, you need to cast it to a WindowsIdentity when you USE it, not when you assign it to the Context. – Erik Funkenbusch Sep 24 '12 at 17:01
  • Thank you for your update when you decided to use Application_AuthorizeRequest. I was using AuthenticateRequest too and it was driving me crazy. – Abdulsattar Mohammed Jan 04 '13 at 17:46

2 Answers2

6

Instead of doing it this way, you should override the Application_AuthenticateRequest method in global.asax, then use Current.User rather than HttpContext.Current.User (not sure why, but there is a difference).

Then, an easy way to access this in your controller is to create an extension method? Something like this:

public static class IIdentityExtensions {
    public static IMyIdentity MyIdentity(this IIdentity identity) {
        return (IMyIdentity)identity;
    }
}

then you can just say User.Identity.IMyIdenty().FirstName. You could probably do this as a property as well.

Here is the code I use:

protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
    FormsAuthenticationTicket authTicket = FormsAuthentication
       .Decrypt(authCookie.Value);
    var identity = new MyIdentity(authTicket.Name, "Forms", 
       FormsAuthenticationHelper.RetrieveAuthUserData(authTicket.UserData));
    Context.User = new GenericPrincipal(identity, 
       DependencyResolver.Current.GetService<ISecurityHandler>()
          .GetRoles(identity.Name).ToArray());
}

Now, ignoring the DependencyResolver stuff and the custom auth ticket stuff, this is pretty basic and works correctly for me.

Then, in my app, when i'm need info from my custom identity, i just cast it with ((IMyIdentity)User.Identity).FirstName or whatever I need. It's not rocket science, and it works.

Erik Funkenbusch
  • 92,674
  • 28
  • 195
  • 291
  • I tried just the authenticaterequest part and got: HttpContext.Current.User is null and Current does not exist. `protected void Application_AuthenticateRequest(object sender, EventArgs e) { if (HttpContext.Current.User.Identity != null && HttpContext.Current.User.Identity.IsAuthenticated) { HttpContext.Current.User = new CPrincipal(HttpContext.Current.User.Identity); } }` – Toby Jones Sep 24 '12 at 17:28
  • @TobyJones - see my edit. It's your job to do the authentication in AuthenticateRequest. The User is of course Null because no authentication has yet occured. – Erik Funkenbusch Sep 24 '12 at 18:41
2

What am I doing wrong?

Probably the [Authorize] attribute is overriding your changes. So instead of doing this in the WindowsAuthentication_OnAuthenticate method in your Global.asax write a custom Authorize attribute, like so:

public class MyAuthorizeAttribute : AuthorizeAttribute
{
    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        var authorized = base.AuthorizeCore(httpContext);
        if (!authorized)
        {
            return false;
        }


        var user = httpContext.User as WindowsIdentity;
        CPrincipal cPrincipal = new CPrincipal(user);
        httpContext.User = cPrincipal;

        return true;
    }
}

and then use your custom attribute instead of the default one:

[MyAuthorize]
public ActionResult SomeAction()
{
    // User.Identity will be your custom principal here
}

In ASP.NET MVC the standard way to perform authorization is through authorization action filters, not through events in Global.asax.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • Authorize attribute doesn't override your context. – Erik Funkenbusch Sep 24 '12 at 16:14
  • 1
    Anyway, it's bad practice to use events in `Global.asax` to perform authorization in an ASP.NET MVC application. You should use custom authorization filters instead. – Darin Dimitrov Sep 24 '12 at 16:14
  • 1
    He's not doing authorization in his code, he's just applying his custom principal, which afaik should still be done in global.asax. – Erik Funkenbusch Sep 24 '12 at 16:19
  • No, I don't think that applying the custom Principal in Global.asax is a good idea. This code is completely untestable in isolation and difficult to be applied conditionally on certain controller actions only. What if you wanted to apply authorization only on certain areas? Global.asax is just not the right way to perform authorization in an ASp.NET MVC application. That's my opinion of course. I respect yours and you are free to do authorization wherever you want. – Darin Dimitrov Sep 24 '12 at 16:20
  • 1
    Well, I've never encountered an app that uses different kinds of principals in different parts of the app, but I suppose if that's what you need, sure. However, I feel that the custom principal should be set as early as possible in the event chain to prevent situations where one event sees one principal but another event sees a different one. It's relatively easy to create a testable wrapper around this, but that's the point of it being an interface anyways. But yeah, i guess we have different opinions on this. – Erik Funkenbusch Sep 24 '12 at 16:27
  • Great, and what was the result? – Darin Dimitrov Sep 24 '12 at 16:36