Search code examples
.net-coremicroservicesidentityserver4ocelot

Ocelot, Identity, IdentityServer4 and API Resource, how to grant access on a role basis


I am trying to learn how a microservices approach with net core should be. So after a lot of tutorials this is what I have got so far:

  1. I have created two apis projects: Company and Package. Ports 5002 and 5010

  2. I need a gateway, Ocelot is the one. I have this configuration:

{

  "Routes": [
    // Company Api
    {
      "DownstreamPathTemplate": "/api/company/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5002
        }
      ],
      "UpstreamPathTemplate": "/api/company/{everything}",
      "UpstreamHttpMethod": [
        "GET"
      ],
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "TestKey",
        "AllowedScopes": []
      },
      "AddHeadersToRequest": {
        "CustomerId": "Claims[sub] > value"
      }
    },
    // Anonymous Company Api
    {
      "DownstreamPathTemplate": "/api/company/getHomeSections",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5002
        }
      ],
      "UpstreamPathTemplate": "/api/anonymous/company/getHomeSections",
      "UpstreamHttpMethod": [
        "GET"
      ]
    },
    // Package Api
    {
      "DownstreamPathTemplate": "/api/package/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5010
        }
      ],
      "UpstreamPathTemplate": "/api/package/{everything}",
      "UpstreamHttpMethod": [
        "GET"
      ],
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "TestKey",
        "AllowedScopes": []
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:5000"
  },
  "AllowedHosts": "*"
}
  1. I have a identityServer4 configured with identity and mongoDB. Works like a charm.

I have configured a client in angular 10. I can log in, I get the token and everything works as expected.

Now my big question is, how do I grant access to users on a API / Role basis? So lets say Role A has access to company/read and Role B to company/read and company/write?

How do actually achieve that?

Thanks a lot.


Solution

  • how do I grant access to users on a API / Role basis?

    There are two ways:

    On the client

    This method doesn't involve the gateway at all, just IdentityServer.

    If you just want role-based behaviour in the UX - to allow/disallow certain calls or to hide/show certain screens/components, you can do the following:

    1. Model your roles as user claims in Identity Server
    2. Have the UX interrogate the bearer JWT returned at login and unpack the claims collection
    3. Apply your role-based UX logic based on the discovered role(s).

    In the gateway/on the back-end

    This is more complicated, but if you want to have role-based behaviour in your back-end then you will need to code for it in your services. One way of doing this is to have each service accept a collection of roles as a header parameter, for example. Then you can:

    1. Model your roles as user claims in Identity Server
    2. In Ocelot expose the upstream service path without any roles header parameter
    3. When the UX makes a request, have Ocelot inject the "roles" claims as a downstream http header (in the same way you are currently doing with the customerId)
    4. Then you can have each service make a decision about whether to allow the request based on the roles

    However, this feels clumsy. Thinking about access control at the service-level, roles feel a bit too coarse-grained.

    Maybe a third way...

    So, role-based behaviour works well on your front-end, but what about your back-end?

    Since you are already taking advantage of the ability to inject the CustomerId user claim into the downstream service, we should consider another solution.

    Rather than role based behaviour on your back-end, why not just use the resource identifier to control access? For example, a private http operation to do with a customer can be defined with the customerId as part of the path:

    GET /customers/{customerId}
    

    In Ocelot, the upstream path can be exposed as

    GET /customers
    

    The customerId claim is injected into the downstream path when the request is received on the gateway (note: Ocelot does not support path parameter injection out-of-the-box. You need to create a class derived from DelegatingHandler, and use it with the DelegatingHandlers[] collection in the ocelot.json to do this).

    In a similar vein, you can secure your company API by creating an Ocelot route from GET /company to GET /company/{companyId} where companyId is a user claim.

    Combined with role-based behaviour on your front-end, this approach gives you much finer-grained access control on your gateway/back-end than you can get with role-based behaviour. You get the best of both worlds.