A few weeks ago I was looking into authentication and authorization architecture patterns for microservice ecosystems and found this lonely, unanswered stack overflow question on the topic.
This piece is a long overdue discussion of the questions they raised.
If video is more your style, check out a lightning talk I recorded on YouTube.
To manage authentication in a microservice context you probably want:
- A User Service to store user data including permissions
/authenticateendpoint on the service for authenticating
Managing authorization is tricker. Here are two paths:
- Create an
/authorizeendpoint in the User Service that other APIs can call to allow/deny access
- Best option if your microservice ecosystem needs to support an arbitrary number of languages/frameworks and you have engineering resources to build a service & endpoint registry
- Send blob of user payload data in
/authenticateresponse and allow downstream services to parse and pass to business logic.
- Best option if all your APIs are in a single language/framework and can share business logic code
Authentication means identifying who is making a request against one of our APIs.
Authorization means determining what a user can do in our system once they have been identified.
Microservice Single Responsibility
A microservice should operate in a single domain. This means a microservice generally should not be responsible for two things. For instance, not user management and reporting.
Single Source of Truth
For any piece of data in our system there should be a single source of truth.
Django as a monolith has an entire framework for authentication and authorization. Can these freely available tools be leveraged in a microservice context?
Regardless of how we decide to handle authorization, we will want a single place to manage our users.
This implies that at the very least we should have a separate service responsible for user management and authentication.
Let’s call this endpoint
While processing a request in downstream APIs, we can forward a bearer token to this endpoint to determine what user is making a request to any given microservice.
If we have an API gateway, this is is a great place to do this.
Determining whether a user is authorized to do something or not in our system is trickier.
Here we have at least two paths:
- Put permission business logic in external service
- Keep permission business logic local to each service
Breaking Out Permissioning Business Logic Into an /authorize Endpoint
Reasoning purely from our principle of single responsibility, we might think about a
/authorize endpoint in our User microservice.
On each request, after authenticating with the
/authenticate endpoint, we could make another call to
/authorize to determine whether or not the user is authorized to perform the given action they are trying to.
Obviously we could probably combine the
/authorize endpoints to speed things up, but for the sake of conceptual clarity let’s keep them separate for the time being.
This involves more or less creating a registry of services and endpoints within our microservice architecture and then linking these to permissions or groups of permissions.
When a request comes in to a downstream API, it can check with the
/authorize endpoint to approve or deny a user’s request.
Analysis of the
/authorize endpoint approach
This is a great option if your microservice ecosystem contains codebases in many different languages or formats. It abstracts the business logic of allowing and denying requests into a single entity
This aligns well with our Single Responsibility Principle as well.
The biggest disadvantage to building this kind of system is that it requires engineering lift to build out a registry of services and endpoints and a mechanism to tie them into your permission system.
While by no means insurmountable, it may not be the best way of allocating engineering time for the business.
Additionally, keeping tests working and up to date in both the user service and downstream APIs becomes more complicated when permissioning is abstracted into a separate service.
In particular, how do you know that the configurations in your user service match the real world deployments of your downstream APIs? How can you ensure that a new release will not accidentally cut users off from endpoints they are authorized to access?
These are all questions worth thinking about.
Delegate Authorization To Downstream APIs
Another approach worth mentioning is delegating permissioning business logic to downstream APIs.
As I mentioned earlier, many frameworks such as Django come with an entire system for managing authentication and authorization in a monolith context.
If we want to skip building out the business logic of an
/authorize endpoint, one alternative is to have the user management service send back a list of user permissions in its
The downstream API can then read the user’s permissions and plug them into its own business logic for permissioning.
Analysis of Delegated Authorization Business Logic
One of the big advantages of a solution like this is that downstream APIs using frameworks like Django can simply plug the permission payload into their existing system.
If you are managing a limited number of APIs, and especially if those APIs are written in the same language and framework, this can be a really easy, low-lift way of managing permissioning across an API ecosystem.
As stated before, if you are working in a multi-language, multi-framework environment you may not want to keep re-writing systems for interpreting the permission blob in each language or framework.