Microservice architectures designed around multiple distributed services are becoming increasingly popular for a wide range of use cases. But, they can carry significant authentication challenges.
In this article, I’ll analyze the unforeseen challenges of a seemingly simple approach to authentication, in which each microservice is responsible for handling its own authentication needs.
Using the example of a recent ThoughtWorks project, I’ll show you how adopting an authentication sidecar architecture can solve these challenges by decoupling authentication logic and service logic.
The ThoughtWorks team was building a one-stop shopping portal site that integrated multiple business systems. These systems were exposed as microservices that were deployed on Kubernetes, an open-source platform for automating the deployment, scaling, and management of containerized applications.
Kubernetes organizes the containers that make up an application into logical units for easy management and discovery. The two foundational components of Kubernetes are Pods, which are the smallest deployable unit of computing that you can create and manage, and Services, which enable you to expose an application running on a set of Pods as a network service.
In this project, each subsystem was independently developed as a Kubernetes Service by different teams using different technology stacks. In this architecture, the front-end client of the portal needs to access different Services, and each Service needs to access different downstream systems to complete the corresponding business scenarios.
As a result, successful interaction with the portal involves user authentication across the frontend, each Kubernetes Service, and downstream business systems — a significant and challenging authentication demand.
At the beginning of the project, there were only a small number of Services integrated into the portal. Since the team wanted to build out each Service quickly, they opted for a self-authentication approach, in which each Service is responsible for handling its own inbound and outbound request authentication.
In this approach, when handling an inbound request — such as one from a frontend application — the Service is responsible for verifying the frontend access token. The complete process is illustrated in the following diagram.
The frontend system obtains the access token to access the backend Service from the OAuth server.
The frontend application calls the Service API with the access token.
The API request flows into the Service.
The Service obtains the public key from the OAuth server and verifies whether the access token is valid. The public key is used to verify the digital signature of the access token. If the access token is valid, the service performs business processing and, if necessary, communicates with downstream services to complete the relevant business scenarios.
When making outbound requests — such as those to downstream business services — the Service needs to use the access token to access backend microservices. The process is illustrated in the following diagram.
The Service obtains an access token using the client’s ID and secret.
The Service uses the access token to make requests to backend services.
While it appears streamlined, the self-authentication approach suffers from multiple drawbacks:
Coupling: Each Service is dependent on an authentication SDK to obtain and verify tokens. Since the Services are based on different programming languages, the authentication SDK of the corresponding language for each service needs to be imported.
Complexity: The Service is responsible for the identity authentication between services as well as the acquisition of access tokens. This increases the complexity of the Service code.
Reusability: Each Service needs to implement the same authentication logic. Since the site has many business domains and Services using different programming languages, authentication logic cannot be reused across Services.
Maintainability: Whenever the OAuth protocol is upgraded, the code associated with authentication for each Service needs to be changed.
After identifying these drawbacks of the self-authentication approach, the team decided to introduce an authentication sidecar pattern to their architecture.
The sidecar pattern is a decoupling pattern, in which supporting functions of the Service are provided by sidecars attached to the main Service. Sidecars are located in the same Kubernetes Pod and share the same lifecycle as their associated Service, being created and terminated alongside the Service.
In this re-factor, the responsibility for handling authentication shifted from the Service itself into sidecars attached to the Service. The team created two distinct sidecars: an ingress sidecar for processing inbound traffic to the attached Service, and an egress sidecar for processing outbound traffic from the attached Service to downstream services.
In the end, this provided a suitable resolution to the challenges of self-authentication, with far fewer drawbacks. In part two I’ll discuss the details of the ingress and egress authentication sidecars, and share the key takeaways of the project as a whole.
Disclaimer: The statements and opinions expressed in this article are those of the author(s) and do not necessarily reflect the positions of Thoughtworks.