9

I'm creating a Web API with users having different roles, in addition as any other application I do not want User A to access User B's resources. Like below:

Orders/1 (User A)

Orders/2 (User B)

Of course I can grab the JWT from the request and query the database to check if this user owns that order but that will make my controller Actions' too heavy.

This example uses AuthorizeAttribute but it seems too broad and I'll have to add tons of conditionals for all routes in the API to check which route is being accessed and then query the database making several joins that lead back to the users table to return if the request Is Valid or not.

Update

For Routes the first line of defense is a security policy which require certain claims.

My question is about the second line of defense that is responsible to make sure users only access their data/resources.

Are there any standard approaches to be taken in this scenario ?

Community
  • 1
  • 1
Mozart Al Khateeb
  • 1,536
  • 1
  • 12
  • 26
  • 1
    You can use route like "users/{userId}/orders/{orderId} and use SQL query like this "where OwnerId = @userId and OrderId = @orderId". In additional add authorization filter to check that the "userId" route parameter value the same as authenticated user. – Sergey Vishnevskiy Apr 22 '20 at 06:42
  • 1
    @SergeyVishnevsky Well that's an idea, but i'll end up with the user id in each and every route when I could just grab the UserId from the httpContext, and i'll have to add custom routes for administrators who do not own the orders. I'm trying to find a cleaner standard approach. – Mozart Al Khateeb Apr 22 '20 at 06:46
  • What do you want to achieve exactly? Do you want to allow user to get **only his** orders? Or do you want to create user roles for different orders? – Oleg Kyrylchuk Apr 22 '20 at 06:51
  • As I see policy based authorization with multiply handlers https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-3.1#why-would-i-want-multiple-handlers-for-a-requirement can help. You can use "ownerId" route parameter as the indicator who is the resource owner for all routes and add two or more AuthorizationHandler for a authorization requirement. The first check "the current user Id equals to "ownerId" route value", the second check "the current user is admin". If one of these handlers approved the requirement than request is authorized. – Sergey Vishnevskiy Apr 22 '20 at 06:59
  • @OlegKyrylchuk Please see updated Question – Mozart Al Khateeb Apr 22 '20 at 07:45
  • @SergeyVishnevsky I'll check that soon – Mozart Al Khateeb Apr 22 '20 at 07:46

6 Answers6

4

Using [Authorize] attribute is called declarative authorization. But it is executed before the controller or action is executed. When you need a resource-based authorization and document has an author property, you must load the document from storage before authorization evaluation. It's called imperative authorization.

There is a post on Microsoft Docs how to deal with imperative authorization in ASP.NET Core. I think it is quite comprehensive and it answers your question about standard approach.

Also here you can find the code sample.

Oleg Kyrylchuk
  • 1,129
  • 1
  • 10
  • 21
4

The approach that I take is to automatically restrict queries to records owned by the currently authenticated user account.

I use an interface to indicate which data records are account specific.

public interface IAccountOwnedEntity
{
    Guid AccountKey { get; set; }
}

And provide an interface to inject the logic for identifying which account the repository should be targeting.

public interface IAccountResolver
{
    Guid ResolveAccount();
}

The implementation of IAccountResolver I use today is based on the authenticated users claims.

public class ClaimsPrincipalAccountResolver : IAccountResolver
{
    private readonly HttpContext _httpContext;

    public ClaimsPrincipalAccountResolver(IHttpContextAccessor httpContextAccessor)
    {
        _httpContext = httpContextAccessor.HttpContext;
    }

    public Guid ResolveAccount()
    {
        var AccountKeyClaim = _httpContext
            ?.User
            ?.Claims
            ?.FirstOrDefault(c => String.Equals(c.Type, ClaimNames.AccountKey, StringComparison.InvariantCulture));

        var validAccoutnKey = Guid.TryParse(AccountKeyClaim?.Value, out var accountKey));

        return (validAccoutnKey) ? accountKey : throw new AccountResolutionException();
    }
}

Then within the repository I limit all returned records to being owned by that account.

public class SqlRepository<TRecord, TKey>
    where TRecord : class, IAccountOwnedEntity
{
    private readonly DbContext _dbContext;
    private readonly IAccountResolver _accountResolver;

    public SqlRepository(DbContext dbContext, IAccountResolver accountResolver)
    {
        _dbContext = dbContext;
        _accountResolver = accountResolver;
    }

    public async Task<IEnumerable<TRecord>> GetAsync()
    {
        var accountKey = _accountResolver.ResolveAccount();

        return await _dbContext
                .Set<TRecord>()
                .Where(record => record.AccountKey == accountKey)
                .ToListAsync();
    }


    // Other CRUD operations
}

With this approach, I don't have to remember to apply my account restrictions on each query. It just happens automatically.

Matt Hensley
  • 831
  • 8
  • 18
  • In this case all the entities with implement the IAccountOwnedEntity ? then I have the user Id in all tables ? – Mozart Al Khateeb Apr 25 '20 at 11:21
  • Yes, that is correct. In theory, if a record is specific to a certain account they would be tied together in the database. – Matt Hensley Apr 26 '20 at 14:55
  • this seem over engineering to me... Obviusly it depends of many entities one might have and how many different queries one have... but it's two class two interfaces for one where – L.Trabacchin Apr 29 '20 at 12:54
  • also the question stated "Of course I can grab the JWT from the request and query the database to check if this user owns that order but that will make my controller Actions' too heavy." this answer does not address that, mine did not aswell, but at least i explained why ... – L.Trabacchin Apr 29 '20 at 13:03
  • Also this kind of approach tend to make your project more rigid, set... real world scenarios can't all fit with an owner definition, it's likely that you gonna need creators, editors, administrators, company who administer those entities... I would not follow this approach, just my two cents... – L.Trabacchin Apr 29 '20 at 13:13
3

To make sure User A cannot view Order with Id=2 (belongs to User B). I would do one of this two things:

One: Have a GetOrderByIdAndUser(long orderId, string username), and of course you take username from the jwt. If the user does't own the order he wont see it, and no extra db-call.

Two: First fetch the Order GetOrderById(long orderId) from database and then validate that username-property of the order is the same as the logged on user in the jwt. If the user does't own the order Throw exception, return 404 or whatever, and no extra db-call.

void ValidateUserOwnsOrder(Order order, string username)
{
            if (order.username != username)
            {
                throw new Exception("Wrong user.");
            }
}
Daniel Stackenland
  • 2,619
  • 1
  • 16
  • 22
2

You can make multiple policies in the ConfigureServices method of your startup, containing Roles or, fitting to your example here, names, like this:

AddPolicy("UserA", builder => builder.RequireClaim("Name", "UserA"))

or replace "UserA" with "Accounting" and "Name" with "Role".
Then restrict controller methods by role:

[Authorize(Policy = "UserA")

Of course this in on the controller level again, but you don't have to hack around tokens or the database. This will give your a direct indicator as to what role or user can use what method.

Squirrelkiller
  • 1,557
  • 1
  • 15
  • 29
  • Please see updated question, Roles do not guarantee that user A can not access user B's data, Perhaps policies can be used in a different way to consider current session's user in it's validation, I'll research that. – Mozart Al Khateeb Apr 22 '20 at 07:49
  • 1
    Well another layer depends on where you want to define who what data belongs to. You could have a persistence layer for example, which takes a users token as additional argument to check against the user saved in the database with that data. – Squirrelkiller Apr 22 '20 at 17:01
  • Policy names on the [Authorize] have to be const values, which means you would have to have a new const value for each user you wanted to add. That doesn't seem like it would scale past 1 or two users very well. – Matt Hensley Apr 23 '20 at 20:45
  • 1
    @MattHensley Which is exactly why I added the "Role" idea. Just put "UserA" there to technically fit OP's question. – Squirrelkiller Apr 23 '20 at 20:47
1

Your statements are wrong, and you are also designing it wrong.

Over optimization is the root of all evil

This link can be summarized in "test the performance before claiming it won't work."

Using the identity (jwt token or whatever you configured) to check if the actual user is accessing the right resource (or maybe better to serve just the resources it owns) is not too heavy. If it becomes heavy, you are doing something wrong. It might be that you have tons of simultaneous access and you just need to cache some data, like a dictionary order->ownerid that gets cleared over time... but that doesn't seem the case.

about the design: make a reusable service that can get injected and have a method to access every resource you need which accept an user (IdentityUser, or jwt subject, just the user id, or whatever you have)

something like

ICustomerStore 
{
    Task<Order[]> GetUserOrders(String userid);
    Task<Bool> CanUserSeeOrder(String userid, String orderid);
}

implement accordingly and use this class to sistematically check if the user can access the resources.

L.Trabacchin
  • 1,220
  • 1
  • 14
  • 31
  • Well great the aim of this question is to hear about standard approaches and to point what is wrong, This API is still under construction, in the past I used to send the user Id along the query to the database. So about ICustomerStore is it an IService or IRepo which responsibility is to query data ? – Mozart Al Khateeb Apr 23 '20 at 06:58
  • Weeell it depends ... My experience is to get things done as fast as you can following just some patterns... Then refactor what you feel need some extra attention. so don't put too many layers. a good way to decide and get also some test done is write the requesites in form of tests, this will give you some insight on how many layers you'll need – L.Trabacchin Apr 23 '20 at 10:14
  • do you use some orm ? like entity framework ? i usually design those stores as a layer between entity framework (the db) and the controller/webapi so the latter don't have to knwo anything of the data source, swapping the store with a mock should work aswell, if you feel the urge to access the database just add some methods to the store – L.Trabacchin Apr 23 '20 at 10:37
  • Yes I use entity framework, based on your description this could be a service layer that encapsulate db queries alongside the business logic. – Mozart Al Khateeb Apr 23 '20 at 14:42
  • absolutely yes, that data could come from any source, you just design a socket. Then if it's not fast enough you'll design a cache for your service, could be another smaller table in the db, could be in memory, whatever you like and works. – L.Trabacchin Apr 23 '20 at 14:49
1

The first question you need to answer is "When I can make this authorisation decision?". When do you actually have the information needed to make the check?

If you can almost always determine the resource being accessed from route data (or other request context), then an policy with matching requirement and handler may be appropriate. This works best when you are interacting with data clearly silo'd out by resource - as it doesn't help at all with things like filtering of lists, and you'll have to fall back to imperative checks in these cases.

If you can't really figure out whether a user can fiddle with a resource until you've actually examined it then you are pretty much stuck with imperative checks. There is standard framework for this but it isn't imo as useful as the policy framework. It's probably valuable at some point to write an IUserContext which can be injected at the point you query you domain (so into repos/wherever you use linq) which encapsulates some of these filters (IEnumerable<Order> Restrict(this IEnumerable<Order> orders, IUserContext ctx)).

For a complex domain there won't be an easy silver bullet. If you use an ORM it may be able to help you - but don't forget that navigable relationships in your domain will allow code to break context, particularly if you haven't been strict on trying to keep aggregates isolated (myOrder.Items[n].Product.Orderees[notme]...).

Last time I did this I managed to use the policy-based-on-route approach for 90% of cases, but still had to do some manual imperative checks for the odd listing or complex query. The danger in using imperative checks, as I'm sure you are aware, is that you forget them. A potential solution for this is to apply your [Authorize(Policy = "MatchingUserPolicy")] at controller level, add an additional policy "ISolemlySwearIHaveDoneImperativeChecks" on the action, and then in your MatchUserRequirementsHandler, check the context and bypass the naive user/order matching checks if imperative checks have been 'declared'.