Including current user into every call to service layer from controller

Viewed 2273

I perform model validation in my controllers, but a second business validation needs to take place at the service/business level. This is usually related to user permission: does the current user have access to the customer/order info he is trying to get or post?

My first (and working) approach is to pass either the entire User instance or its Id (by calling User.Identity.GetUserId()), which would be enough most -if not all- of the time. So I'll have something like this:

public IHttpActionResult Get(int id)
{
    try
    {
        var customer = customerService.GetById(id, userId);
        return Ok(customer);
    }
    catch (BusinessLogicException e)
    {
        return CreateErrorResponse(e);
    }
}

But I don't really like the fact that with this approach I'm going to have to include an extra parameter in pretty much every call to my service layer. If I am calling a GetById() method, I want to get something by providing an ID, not an ID and a user ID.

A simple workaround would be something along these lines, which also works:

public IHttpActionResult Get(int id)
{
    customerService.SetCurrentUser(User.Identity.GetUserId());

    try
    {
        var customer = customerService.GetById(id);
        return Ok(customer);
    }
    catch (BusinessLogicException e)
    {
        return CreateErrorResponse(e);
    }
}

But instead of having to make a separate call to set the current user, I'd like this to be done automatically with every call to the service. How can I do it?

Here's what my service looks like:

public class CustomerService : EntityService<Customer>, ICustomerService
{
    public string UserId;
    IContext context;
    public CustomerService(IContext context) : base(context)
    {
        this.context = context;
        this.dbSet = context.Set<Customer>();
    }

    public void SetCurrentUser(string userId)
    {
        UserId = userId;
    }

    public DTO.Customer GetById(int id)
    {
        if (!IsAccessibleByUser(id))
        {
            throw new BusinessLogicException(ErrorCode.UserError, "UserId: " + UserId);
        }

        return dbSet.FirstOrDefault(x => x.Id == id).ToDto<Customer, DTO.Customer>();
    }

    public bool IsAccessibleByUser(int id)
    {
        return context.UsersAPI.Any(a => a.AspNetUsersID == UserId);
    }
}
5 Answers

Late answer but I understand your concern, best practice you want to achieve in your code and make it as clean as possible, I had exactly the same issue as yours long time ago and same as you I felt that this is not right, there should be a better way to do it, and in my case I used on all my layers:
System.Threading.Thread.CurrentPrincipal.Identity
and this answer on SO describe it: https://stackoverflow.com/a/27636802/20126

and while this is the way I always use, it worth looking at what others did as well:
Accessing HttpContext and User Identity from data layer
How to get User ID inside a separate assembly
Retrieving the current logged in user's Id in a class library outside of an ASP.NET Webforms App using forms authentication
Get the ID of the current user ASP.NET Membership

You can directly get the User Principal from

HttpContext.Current.User

in your services. You need to set your User Principal in the authorization layer after you check for the token validation or somewhere prior to calling the service.

But, I feel this makes your service more constrained to use some specific authentication procedure and you will not be able to use this service function in some other application unless the User Principal is not set.

In startup.cs add below to ConfigureServices:

services.AddHttpContextAccessor();

and then in constructor of your class inject IHttpContextAccessor and after that you can use it like this:

context.HttpContext.User.Identity.GetUserId() 
Related