4

I'm working on a .Net core API for product configuration and ordering. The ticket for Permissions has come up and I've been tasked with developing a solution, ideas of middleware have been mentioned.

Considering:

  • Clean Architecture
  • Don't repeat yourself
  • Singular Responsibility

When should I, (or at which layer should I) deal with access control? For example, A customer can cancel an order, the Presentation layer takes care of the fact that they are a customer, they have permission to orders with enough clearance to cancel one. But I also need to check that the Order they wish to cancel is owned by them, i.e. a matching AccountID between user and order.

The Repository layer is responsible for fetching/storing data and doesn't care who's doing it. The Application layer cares about how to do the job, i.e. what must happen to cancel the order?

In the Clean Architecture examples I've seen no one talks about actually checking to see if the user making the request is allowed to modify/create the object they're trying to. It seems to me like a layer is missing, an access control layer.

The question I am asking is: Does anyone have any experience with access control in Clean Architecture and if so where did you put it and what were the pitfalls/benefits of doing it that way?

P.S: Using JWT for authorization.

3 Answers 3

3

Security is a cross-cutting concern that can span several layers, and it does in different formats and ways from layer to layer.

For example

Blue & Green circles

We implement how actors are authenticated (a way for them to prove they are who they claim to be and be electable to use the application) and authorized (a way to regulate the interaction based on rules or policies).

At this level, the entity associated with security is not the customer; it's the well-known Account. In a nutshell, and from the security context standpoint, there are no customers, only accounts and privileges.

This gets more complicated if accounts support multi-tenancy (multiple users per account).

For multi-tenant accounts, for one user to act on behalf of a partner, it must do it through the proper security function, so the application security is always aware of who was authenticated and who is the enabled user at the time.

The Role of the user can be implemented at this level too. For example, there can be different types of accounts (Admin, Customer, Provider, Guest).

If roles (accountabilities and responsibilities) are intrinsic to the domain or business, we implement these and the rules associated in inner circles (red, yellow).

Red circle

Here, we evaluate (among other rules or policies) the tenancy (data ownership). For example,

  • CancelOrder use case evaluates OrderTenantcyPolicy and ConfirmationPolicy to validate the tenancy and evaluate the confirmation.
  • ListCustomerOrders use case applies OrderTenantcyPolicy to retrieve a very specific subset of orders.

Yellow circle

Here we might find the abstractions (ports) of those policies and rules I mentioned previously. Concrete implementations (adapters) are left to outer circles for customization and extensibility, but the evaluation of these policies and rules is executed here.

Show Case

When should I, (...) deal with access control? For example, A customer can cancel an order, the Presentation layer takes care of the fact that they are a customer, they have permission to order with enough clearance to cancel one. But I also need to check that the Order they wish to cancel is owned by them, i.e. a matching AccountID between the user and order.

Action Scope/Layer
Authentication Blue & Green
Authorize user to execute CustomerOrderList use case Blue & Green
Execute use case security policies and rules Red & Yellow
Authorize user to execute CancelOrder use case Blue & Green
Execute use case security policies and rules Red & Yellow

1: Conceptually simple, but complex to implement due to the so many abstractions, models, mappings, etc

4
  • It sounds like you're advocating for a layer responsible for filtering the data requested/submitted between the application layer and the infrastructure?
    – Tod
    Commented May 25, 2023 at 15:30
  • I'm not advocating, I'm explaining my experience with pure CA. If you take CA to the extreme (the dogmatic implementation) each layer implements security as best serves and it's up to you how to tie everything in the application layer. The pragmatic implementation will rely on libraries of frameworks, which implementation can spend several layers too but it's the framework (not you) who ties everything, so you only do the configuration.
    – Laiv
    Commented May 25, 2023 at 15:40
  • CA doesn't have a business layer defined in the diagrams but does talk about business logic living in the App layer. My "advocating" remark was aimed at a conversation regarding adding in a new layer or augmenting the existing App layer?
    – Tod
    Commented May 26, 2023 at 11:17
  • Yes, CA doesn't refer to layers. Layers are mere organizational units we use to arrange code belonging to one or more of the CA "circles" we see in the diagram. I edited the answer to refer the circles instead of layers (layers are a bit subjective as you pointed)
    – Laiv
    Commented May 26, 2023 at 14:16
1

Authentication

I realize that they may be API's which can be accessed anonymously. However for the purpose of this answer I am considering that anonymous access is out of scope. Hence my assumptions are that:

  • The security system has a mechanism to authenticate the user/device.
  • Most API's can quickly reject any request that is unauthenticated.
  • That for the rest of this answer, we are basically only talking about authorization checks for authenticated users.

Given that, I see three major objectives of the security infrastructure:

  1. The domain model is authoritative.
  2. Provide defense in depth (and some protection from DOS)
  3. Improve end user experience and improve threat detection.

Note: I using domain model here in the context of DDD.

Authoritative Domain Model

If you need more than simple RBAC (Role based access control), it's likely that the line between business logic and security will become blurred for example:

Managers can only approve PO's with an amount less than $100K, Directors can approve up to $500K

Therefore you are forced to code some of your security into the domain layer, once you start down that path it makes sense to try to keep all security logic together in the domain model - so you don't have to make changes in multiple locations.

There should be appropriate methods in the domain layer (either on the domain objects or repository methods) which can be secured for all mutating operations.

I am generally a fan of CQRS for handling read operations, which usually results in some degree of duplicated security logic. A pattern you may chose to implement is to return the user_id with every record (i.e. you do enough joins in the DB to get back to a user_id **) in a lot of calls the user_id of the record will match the user_id from the security context - i.e. you can add logic to fail/alert if it doesn't.

Note: This is an explicit check vs passing the user_id into the query (sure you can screw up the query either way), however by explicitly returning the user_id it's less likely you will leave a hole by omission.

** - That or you have a user_id field in every table.

Defense in Depth

It could be expensive to load the entire domain model simply to do a permissions check. As already stated unauthenticated requests are dropped early, this can be combined with rate limiting (keyed on the authenticated user) to provide some protection against DOS attacks (although you may still want the protections provided by a CDN).

You can also use your infrastructure to prevent certain types of attack, for example if you use the BFF (Backend for frontend) model. If a BFF is designed specifically for end users, the end user BFF never accepts the user_id as a parameter, instead all calls to the domain layer extract the user_id from the security context (JWT or similar). You would provide a different BFF for administration functions that can modify other users.

You can also use the BFF to filter information returned back to the client - say you have a policy that no data received from an internal API is "passed-though" to the client - it must be deserialized mapped to a BFF object and re-serialized - you can now audit the types of data returned by the BFF simply by looking at the transfer objects.

End User Experience

Assume you have a policy:

Clients/End users should not make requests that result in a permission denied.

Consider the previous case of a manager trying to approve a PO > $100K the UI should be told in advance that the approval will be rejected, hence it can display a message to the user and disable the approve button.

If implemented correctly/broadly this also has the advantage that you can monitor the number of "Permission Denied"s that occur - since normal operation should not generate a significant number of them, any spikes in PDs can be used as an indicator of potential nefarious action.

1
  • I'll update the question to mention we are using JWT for authentication. I don't think we have any situations where multi-level permissions will apply. We have a matrix for permissions (eg, Orders/Quotes/Accounts/Logins to each of the CRUD operations). Regarding the comment about BFF and user_id we are already doing that with the JWT but the permissions up front idea I like.
    – Tod
    Commented May 26, 2023 at 11:14
1

I agree with what has already been said in the previous answers, just will add my three cents and try to simplify this.

At a high-level, you need to differentiate between Authentication and Authorization.

  • Authentication: Identify the user according to the token.
  • Authorization: Decide if the user has the appropriate permission to do the requested action.

Regarding the layers:

  • Authentication: The transport layer (green I guess) is perfect for the authentication. It can be a middleware that is the same for all the APIs that encodes the token into a User Model.

  • Authorization: Basically, it's the application's responsibility. Although theoretically, it can be done in the transport layer middleware as well - for example, the middleware can get a list of required permissions and can check if the user has these.

But if the authorization is based on request data, which can be different between requests, the authorization is the application layer's responsibility.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.