Obtaining the current Principal outside of the Web tier
Asked Answered
G

5

8

I have the following ntier app: MVC > Services > Repository > Domain. I am using Forms authentication. Is it safe to use Thread.CurrentPrincipal outside of my MVC layer to get the currently logged in user of my application or should I be using HttpContext.Current.User?

The reason I ask is there seems to be some issues around Thread.CurrentPrincipal, but I am cautious to add a reference to System.Web outside of my MVC layer in case I need to provide a non web font end in the future.

Update

I have been following the advice recieved so far to pass the username into the Service as part of the params to the method being called and this has lead to a refinement of my original question. I need to be able to check if the user is in a particular role in a number of my Service and Domain methods. There seems to be a couple of solutions to this, just wondering which is the best way to proceed:

  1. Pass the whole HttpContext.Current.User as a param instead of just the username.
  2. Call Thread.CurrentPrincipal outside of my web tier and use that. But how do I ensure it is equal to HttpContext.Current.User?
  3. Stick to passing in the username as suggested so far and then use Roles.IsUserInRole. The problem with this approach is that it requires a ref to System.Web which I feel is not correct outside of my MVC layer.

How would you suggest I proceed?

Guadalquivir answered 18/7, 2012 at 14:37 Comment(0)
P
2

I wouldn't do either, HttpContext.Current.User is specific to your web layer.

Why not inject the username into your service layer?

Polypary answered 18/7, 2012 at 14:39 Comment(6)
So I could inject HttpContext.Current.User into my services with the service accepting an IPrincipal in the ctor?Guadalquivir
Pass the username, populated via HttpContext.Current.User, into the service layer. It can be a string parameter, or even a new object parameter if you need more values. So no, not an IPrincipal. And yes, after successful login.Polypary
If you remove Thread.CurrentPrincipal and use your own POCO you'll have more flexibility and changes will be easier. On the other hand you'll have to replace existing code that may work just fine for your relatively simple requirements. You'll have to decide which path to take based on the project dynamics, it would be good if update the page when you finish so we can see how it went.Polypary
I am still looking into this. The problem I face is that I have, for example, a domain object called Document. Document has a method on it CurrentUserCanEdit() which does the following check: Thread.CurrentPrincipal.IsInRole(documentEditorRole). This seems to function correctly so far. The only alternative seems to be to pass all of the users roles into the Service layer each time a role sensitive method is called which I prefer not to do.Guadalquivir
Why can't the service layer do a look up of the roles?Polypary
Well it could but I am again faced with the problem of either referencing System.web and using HttpContext.Current.User or using Thread.CurrentPrincipal. Plus by keeping it in the domain, I can encapsulate all of the logic behind CurrentUserCanEdit() in one place. This is particularly useful as I can map the result of this method to a viewmodel and thus determine in the view if the edit option is displayed via the same logic that the service checks against before commiting an edit.Guadalquivir
P
2

You should abstract your user information so that it doesn't depend on Thread.CurrentPrincipal or HttpContext.Current.User.

You could add a constructor or method parameter that accepts a user name, for example.

Here's an overly simplified example of a constructor parameter:

class YourBusinessClass 
{
   string _userName;
   public YourBusinessClass(string userName)
   {
      _userName = userName;
   }

   public void SomeBusinessMethodThatNeedsUserName()
   {
      if (_userName == "sally")
      {
         // do something for sally
      }
   }
}
Proudlove answered 18/7, 2012 at 14:39 Comment(4)
Do you mean create my own implementation of IPrincipal? Would this be set by during a successfull login? If so, how would I retrieve it in the business layer?Guadalquivir
No, only the stuff you need in your business layer. Assuming you only need the userName, you could do something like the example I added to my answer. Shyju, Joe R and I are all basically saying the same thing, only give your business layer what it needs.Proudlove
Agreed, we are really saying the same thing. :-)Polypary
So I am right in thinking Thread.CurrentPrincipal should not be used? I have a number of methods on Domain objects such as CanEdit which check against this to ensure user is authenticated. This works nicely as I can automap the result of this to a bool on my Viewmodel. Seems I need to make some big changes if Thread.CurrentPrincipal is removed from my Domain!Guadalquivir
J
2

Map the relevant User details to a new Class to represent the LoggedInUser and pass that as an argument to your Business layer method

 public class LoggedInUser
 {
   public string UserName { set;get;}
   //other relevant proerties
 }

Now set the values of this and pass to your BL method

var usr=new LoggedInUser();
usr.UserName="test value ";  //Read from the FormsAuthentication stuff and Set
var result=YourBusinessLayerClass.SomeOperation(usr);
Jule answered 18/7, 2012 at 14:41 Comment(0)
Y
2

I prefer option number 2( use Thread.CurrentPrincipal outside of web tier ). since this will not polute your service tier & data tier methods. with bonuses: you can store your roles + additional info in the custom principal;

To make sure Thread.CurrentPrincipal in your service and data tier is the same as your web tier; you can set your HttpContext.Current.User (Context.User) in Global.asax(Application_AuthenticateRequest). Other alternative location where you can set this are added at the bottom.

sample code:

    //sample synchronizing HttpContext.Current.User with Thread.CurrentPrincipal
    protected void Application_AuthenticateRequest(Object sender, EventArgs e)
    {
        HttpCookie authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];

        //make sure principal is not set for anonymous user/unauthenticated request
        if (authCookie != null && Request.IsAuthenticated)
        {
            FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);

            //your additional info stored in cookies: multiple roles, privileges, etc
            string userData = authTicket.UserData;

            CustomPrincipal userPrincipal = PrincipalHelper.CreatePrincipal(authTicket.Name, authTicket.UserData, Request.IsAuthenticated);

            Context.User = userPrincipal;
        }
    }

of course first you must implement your login form to create authorization cookies containing your custom principal.

Application_AuthenticateRequest will be executed for any request to server(css files, javascript files, images files etc). To limit this functionality only to controller action, you can try setting the custom principal in ActionFilter(I haven't tried this). What I have tried is setting this functionality inside an Interceptor for Controllers(I use Castle Windsor for my Dependency Injection and Aspect Oriented Programming).

Yahoo answered 29/8, 2012 at 4:55 Comment(0)
C
1

I believe you are running into this problem because you need to limit your domains responsibility further. It should not be the responsibility of your service or your document to handle authorization. That responsibility should be handled by your MVC layer, as the current user is logged in to your web app, not your domain.

If, instead of trying to look up the current user from your service, or document, you perform the check in your MVC app, you get something like this:

if(Roles.IsUserInRole("DocumentEditorRole")){

    //UpdateDocument does NOT authorize the user. It does only 1 thing, update the document.
    myDocumentService.UpdateDocument(currentUsername, documentToEdit);

} else {

    lblPermissionDenied.InnerText = @"You do not have permission 
                                      to edit this document.";

}

which is clean, easy to read, and allows you to keep your services and domain classes free from authorization concerns. You can still map Roles.IsUserInRole("DocumentEditorRole")to your viewmodel, so the only this you are losing, is the CurrentUserCanEdit method on your Document class. But if you think of your domain model as representing real world objects, that method doesn't belong on Document anyway. You might think of it as a method on a domain User object (user.CanEditDocument(doc)), but all in all, I think you will be happier if you keep your authorization out of your domain layer.

Creole answered 7/1, 2013 at 6:7 Comment(3)
I like this answer as it keeps things simple. However, as the MVC layer is only really for presentation, it makes sense that business logic is applied beyond the MVC layer (as a different presentation could be used later on). Also, as each Service class doesn't reference any other Service class, it makes sense to have the logic on a domain class as any Service can then access.Guadalquivir
What happens if I need to change my presentation layer? I've gotta re-implement all the security concerns.Pappano
It's been a long time since this answer, and I probably wouldn't go this route today, but I still think it has merit, because it keeps things simple. The right way to do it will greatly depend on the size of the project, and this might be approiate for a smaller project. If you push authorization into the domain, you also need a 'NotAuthorized' exception hierarchy, or similar mechanism, as well as handling of this feedback in the presentation layer. All told that will be quite a lot of complexity, so you should be sure you need it :-)Creole

© 2022 - 2024 — McMap. All rights reserved.