5

I want to use the ASP.NET MVC 5 for my web app. I need use the windows authentication.

If I use the windows authentication where is the best place for reading user information (userid and roles) and store its to the Session?

I have the method for getting the user information by username from the database like this:

public class CurrentUser
    {
        public int UserId { get; set; }

        public string UserName { get; set; }

        public Roles Roles { get; set; }
    }

    public enum Roles
    {
        Administrator,
        Editor,
        Reader
    }

    public class AuthService
    {
        public CurrentUser GetUserInfo(string userName)
        {
            var currentUser = new CurrentUser();

            //load from DB

            return currentUser;
        }
    }
Jenan
  • 3,408
  • 13
  • 62
  • 105

2 Answers2

3

You've asked two questions (1) the best place to obtain user information and (2) how to store it in the Session. I'll answer (1) and in so doing perhaps show that you need not put any additional information in the session.

You've stated that your application is using Windows Authentication, so that means the hard work of authenticating the user has already been done by IIS/HttpListener before your app receives the request. When you receive the request there will be a WindowsPrincipal in HttpContext.User. This will have the windows username and AD roles already established, but you wish to use additional roles stored in the database...

You could access your AuthService from anywhere in your application, but probably the best approach is to register an IAuthorizationFilter and do the work there. By following this approach, the additional roles and other information you fetch from the database will be available in your controller methods and, perhaps more importantly, from any additional library code that needs to check user credentials.

Prior to .Net 4.5, if you wanted to add additional information to the WindowsPrincipal I think your only choice was to replace the system-provided User with another object that implemented the IPrincipal interface. This approach is still available (and what I recommend), but since the introduction of Windows Identity Foundation (WIF) in .Net 4.5, WindowsPrincipal is derived from  System.Security.Claims.ClaimsIdentityClaimsIdentity, which supports adding additional roles (and other useful information) to the system-provided principal. However, as several people have found, there is a bug/feature in Windows which can cause an exception The trust relationship between the primary domain and the trusted domain failed to be thrown when checking roles that have been added programmatically. We have found that a simple and reliable way to avoid this is to replace the User with a GenericPrincipal.

Steps required:

(1) create an IAuthorizationFilter.

class MyAuthorizationFilter : IAuthorizationFilter
{
    AuthService _authService;

    public MyAuthorizationFilter(AuthService authService)
    {
        _authService = authService;
    }

    public void OnAuthorization(AuthorizationContext filterContext)
    {
        var principal = filterContext.HttpContext.User;

        if (principal.Identity != null && principal.Identity.IsAuthenticated)
        {
            // Add username (and other details) to session if you have a need
            filterContext.HttpContext.Session["User"] = principal.Identity.Name;

            // get user info from DB and embue the identity with additional attributes
            var user = _authService.GetUserInfo(principal.Identity.Name);

            // Create a new Principal and add the roles belonging to the user
            GenericPrincipal gp = new GenericPrincipal(principal.Identity, user.RoleNames.ToArray());
            filterContext.HttpContext.User = gp;
        }
    }
}

(2) Register your filter. This can be registered at the controller level or globally. Typically you will do this in App_Start\FilterConfig.cs:

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new MyAuthorizationFilter(new AuthService()));
    }
}

(3) Use the provided GenericPrincipal in your application code to answer questions about the user identification and other credentials. e.g. in your controller method you can access the username or any other "claims" (e.g. email address) stored in the GenericPrincipal by your filter.

        public ActionResult Index()
        {
            ViewBag.Name = HttpContext.User.Identity.Name;
            if(HttpContext.User.IsInRole("Administrator"))
            {
                // some role-specific action
            } 
            return View();
        }

Because you've used the built-in mechanism to record Principal roles, you can access user details from anywhere using HttpContext.User or System.Threading.Thread.CurrentPrincipal. Also you can use the AuthorizeAttribute in you controller methods to declare which actions are available to certain roles or users. e.g.

   public class HomeController : Controller
    {
        [Authorize(Roles = "Administrator")]
        public ActionResult Admin()
        {
            return View();
        }

See MSDN for further details about ClaimsIdentity

I hope this helps

-Rob

Rob
  • 1,472
  • 12
  • 24
  • Rob Thank you very much for your answer. Your solution of getting the roles is amazing but I honesly need to get userid to te session for reason like get some information from database by userid etc. Can I help me get userid to the session? :-) I don't know where is the best place to do this? Thanks. – Jenan Sep 25 '16 at 06:38
  • Hi Jenan. You can add the add the username to the session in the AuthorizationFilter if you need to (I've updated the answer to show this), but I'm not sure why you would need to do this. You can access the Principal (and from there Identity.Username) from any code using HttpConext.User (in your Controller) or, if you're deeper down in library code, you can use System.Threading.Thread.CurrentPrincipal. – Rob Sep 25 '16 at 09:54
  • Rob, thank you very much for your time. This answer is great for me. – Jenan Sep 25 '16 at 16:55
  • Hi Rob, I'm pretty new to MVC and am trying to implement this. I've got it to work using windows groups, but want to create my own Roles. When I add my own role using `claimsIdentity.AddClaim(new Claim(claimsIdentity.RoleClaimType, "MyCustomRole"));` there is no error, but then I try to use it with a controller `[Authorize(Roles = "MyCustomRole")]` and I get the error `The trust relationship between the primary domain and the trusted domain failed` where it seems to be looking for a windows group. Can you please let me know how I can add non windows groups? Cheers! – David May 18 '17 at 05:29
  • Hi @David. Some time after writing my answer we got the same `trust relationship` error (it started happening after a weekend maintenance on our servers). We worked with our domain admins to try to resolve the problem, but were not successful. I think it might also be a bug in the .Net framework code where it is making an AD call to get details about the role claim-types added programmatically. This only happens when adding claims to the `WindowsPrincipal`, so we changed our code to use a new `GenericPrincipal`. I have updated the answer to reflect this. – Rob May 26 '17 at 08:43
1

First and foremost: never, never, never store user details in the session. Seriously. Just don't do it.

If you're using Windows Auth, the user is in AD. You have use AD to get the user information. Microsoft has an MSDN article describing how this should be done.

The long and short is that you create a subclass of UserIdentity and extend it with the additional properties you want to return on the user:

[DirectoryRdnPrefix("CN")]
[DirectoryObjectClass("inetOrgPerson")]
public class InetOrgPerson : UserPrincipal
{
    // Inplement the constructor using the base class constructor. 
    public InetOrgPerson(PrincipalContext context) : base(context)
    {
    }

    // Implement the constructor with initialization parameters.    
    public InetOrgPerson(PrincipalContext context, 
                         string samAccountName, 
                         string password, 
                         bool enabled)
                         : base(context, 
                                samAccountName, 
                                password, 
                                enabled)
    {
    }

    InetOrgPersonSearchFilter searchFilter;

    new public InetOrgPersonSearchFilter AdvancedSearchFilter
    {
        get
        {
            if ( null == searchFilter )
                searchFilter = new InetOrgPersonSearchFilter(this);

            return searchFilter;
        }
    }

    // Create the mobile phone property.    
    [DirectoryProperty("mobile")]
    public string MobilePhone
    {
        get
        {
            if (ExtensionGet("mobile").Length != 1)
                return null;

            return (string)ExtensionGet("mobile")[0];
        }

        set
        {
            ExtensionSet( "mobile", value );
        }
    }

    ...
}

In the example code above, a property is added to bind to the AD's user's mobile field. This is done by implementing the property as shown utilizing ExtensionSet, and then annotating the property with the DirectoryProperty attribute to tell it what field it binds to.

The DirectoryRdnPrefix and DirectoryObjectClass attributes on the class need to line up with how your AD is set up.

Once this is implemented, then you will be able to get at the values simply by referencing them off User.Identity. For example, User.Identity.MobilePhone would return the mobile field from AD for the user.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • Thank you for answer. Why don't store user info into session? – Jenan Sep 12 '16 at 16:23
  • First, the session in general should be used sparingly, if at all. Sessions are a necessary evil, not at all something you should be using frequently or on a whim. Second, sessions are inherently insecure. There's ways to make them more "secure" by using HTTPS and flags like `HTTPOnly` and `Secure` when setting cookies, but they still expose vulnerabilities that would not exist otherwise. Since we must rely on them for at least things like authentication, the best you can do is mitigate the exposure. That's done by only including the bare necessity of information to retrieve the user. – Chris Pratt Sep 12 '16 at 16:28
  • Something like a user id, for example, is pretty meaningless without access to the database that user id exists in, so including it in a session cookie is relatively harmless, but when you start including things like name, address, email, phone, etc in the session, then you have the potential to leak all that personal information. – Chris Pratt Sep 12 '16 at 16:31
  • Thank you for the explanation. Primary I need load the roles from database by user id. If I decide store only the user id into session then where do you recommend do it? – Jenan Sep 12 '16 at 16:50
  • Chris can you help me with storing the userid into the session? – Jenan Sep 14 '16 at 10:42