简体   繁体   中英

Asp.net Core WebAPI Resource based authorization outside controller level

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?

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.

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.

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.");
            }
}

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.

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.

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'.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM