Let me assume your microservices communicate via HTTP. We hit the same question when designing your microservice approach so let me share that:
First thing we recognized was, that there is no way around handing over some kind of user information with every cross-service request in the backend. Now, how this user information in structured is not that relevant. We kept it very simple. Since we use token authentication we just hand over this exact token from service to service. (Your problem is very similar to this one)
And yes, that approach requires every service to re-enumerate user information for this given token from some kind of token-service
. Since this happens once and only once (make sure of that when reviewing code!!) per request, we decided we'd take this road and optimize the hell out of our token-service
(we'll publish it as opensource soon!). But it also enables some interesting possibilities like services that can use a token of their own to impersonate a user and perform actions on it's behalf, while the service receiving this impersonation token can query the information from the token-service
in exactly the same way it would without impersonation!
To summarize: We hand over the access token in all cross-service calls to keep the overhead in the request to an absolute minimum, adds only a constant number of additional cross-service requests!
Authentication and authorization are always good topics
I will try to explain to you how we deal with authorizations in the current multi-tenant service that I am working. The authentication and authorization are token based, using the JSON Web Token open standard. The service exposes a REST API that any kind of client (web, mobile, and desktop applications) can access. When a user is successfully authenticated the service provides an access token that must be sent on each request to the server.
So let me introduce some concepts we use based on how we perceive and treat data on the server application.
Resource: It is any unit or group of data that a client can access through the service. To all the resources that we want to be controlled we assign a single name. For instance, having the next endpoint rules we can name them as follow:
product
/products
/products/:id
payment
/payments/
/payments/:id
order
/orders
/orders/:id
/orders/:id/products
/orders/:id/products/:id
So let's say that so far we have three resources in our service; product
, payment
and order
.
Action: It is an operation that can be performed on a resource, like, read, create, update, delete, etc. It is not necessary to be just the classic CRUD operations, you can have an action named follow
, for instance, if you want to expose a service that propagates some kind of information using WebSockets.
Ability: The ability to perform an action
on a resource
. For instance; read products, create products, etc. It is basically just a resource/action pair. But you can add a name and description to it too.
Role: A set of abilities that a user can own. For example, a role Cashier
could have the abilities "read payment", "create payment" or a role Seller
can have the abilities "read product", "read order", "update order", "delete order".
Finally, a user can have various roles assigned to him.
Explanation
As I said before, we use JSON Web Token and the abilities that a user possesses are declared in the payload of the token. So, suppose we have a user with the roles of cashier and seller at the same time, for a small retail store. The payload will look like this:
{
"scopes": {
"payment": ["read", "create"],
"order": ["read", "create", "update", "delete"]
}
}
As you can see in the scopes
claim we don't specify the name of the roles (cashier, seller), instead, just the resources and the actions that are implicated are specified. When a client sends a request to an endpoint, the service should check if the access token contains the resource and action required. For example, a GET
request to the endpoint /payments/88
will be successful, but a DELETE
request to the same endpoint must fail.
How to group and name the resources and how to define and name the actions and abilities will be a decision made by the developers.
What are the roles and what abilities will have those roles, are decisions made by the customers.
Of course, you must add extra properties to the payload in order to identify the user and the customer (tenant) that issued the token.
{
"scopes": {
...
},
"tenant": "acme",
"user":"coyote"
}
With this method, you can fine-tune the access of any user account to your service. And the most important, you don't have to create various predefined and static roles, like Admin, Agents, and End-Users as you point out in your question. A Super User will be a user that owns a role
with all the resources
and actions
of the service assigned to it.
Now, What if there are 100 resources and we want a role that gives access to all or almost all of them?. Our token payload would be huge. That is solved by nesting the resources and just adding the parent resource in the access token scope.
Authorization is a complicated topic that must be addressed depending on the needs of each application.
Best Answer
I have a few recommendations based on my experience.
INFO
logs can be very helpful for application logs, and you don't want to hamper your ability to use an appropriate logging level.