We have a few services that are only accessible through the API monolith because that's where we verify a customer owns a subscription. This article is about the solution we found to use these services without the API monolith.
Before we go further, it's important to know that we use JSON Web Tokens (JWT) to represent claims securely between our services and between our frontend and backend. These tokens hold the identity of the user and some permissions. They are signed to guarantee the integrity of the claims contained within.
The following diagram illustrates how a request with a subscription identifier is forwarded to a service. The request goes through the API monolith that queries the subscription and the customer to verify the subscription belongs to the customer before forwarding the requets to the destination service.
I'm designing a backend for frontend (BFF) to replace one of the costliest endpoint of HelloFresh's API monolith. The BFF would use a few services that require a subscription identifier as parameter. Thus, the BFF must be able to trust a user making a request is the actual owner of the subscription before making requests to upstream services. I didn't want to query the subscription from the API monolith because the request can take up to 250 ms, which is the maximum time I wish for an entire request to the BFF.
We considered whether the BFF should do the access control, similarly to the API monolith, or whether the services that require the subscription identifier should secure their resources. We also considered creating a service that would determine if a customer owns a subscription. During the review process, we got the idea of having this access control as an external filter for Ambassador (our new Gateway), something that could benefit any service, even the API monolith eventually.
Proposal
We decided to implement the auth mechanism as an external filter for Ambassador. It would control a customer owns a subscription and allow or deny forwarding a request. Request denial would be much faster, and we would avoid burdening our network with unnecessary, and potentially harmful, requests.
Ambassador filters are defined by host/path matches, using filter policies. The filter would only run for the endpoints we specify and would not burden endpoints that make no use of subscriptions. The filter would be implemented as an external process and run as a sidecar in the same pod, for maximum performance. The filter would read subscription/customer relationships from a datastore that would be updated by our backend monolith.
The following diagram represents our new authorization sequence:
The new design is made of three parts, each with a specific role: the datastore holds the subscription/customer pairs, the backend monolith stores subscription/customer pairs, and the filter for the gateway authorizes requests.
The datastore holds subscription/customer pairs
The subscription/customer relationship is a simple pair that would be stored as key/value. We would use 3 Redis instances with cache mode disabled to guarantee the safety and the longevity of the data.
Regarding key/value pairs, we would have {country}:{subscription_id}
as key, and the customer UUID as value. We need to include the country with the subscription identifier because each country has its own database, therefore a subscription identifier is not unique across countries. We would store the customer UUID as binary (16 bytes), instead of a string (36 bytes), to reduce disk/memory usage and speed up transfers.
In order to optimize the memory used by Redis, we would follow recommendations and use HSET/HGET
commands to store/retrieve entries. It fits perfectly with our key model and persistence strategy.
The backend monolith maintains subscription/customer pairs
The backend monolith maintains subscriptions and would be responsible for creating the subscription/customer pairs in the datastore. We considered using events but judged it too risky, we didn't want a new customer to be greeted with a denial upon landing on the My Deliveries screen because the event couldn't be processed in time.
The sequence of creating a subscription would be as follows:
- If the update of the datastore fails, the monolith rolls-back the transaction.
- If the pod is killed before the transaction is complete, there would be no subscription inserted, but there would be a key in the datastore. Because subscription identifiers are auto-incremented, the next subscription created would fix the datastore.
Filtering requests in the gateway
To determine if a customer owns a subscription, the filter would need three things: the country, the customer identifier, and the subscription identifier. The filter would extract the country and the customer identifier from the token, and the subscription identifier from the path. If it's a match the filter would grant the access, otherwise would deny it.
Conclusion
We created an authorization filter for Ambassador, and we are now able to control requests without involving the API monolith. We could even use the filter to control access to the API monolith, and remove the authorization code from it. With Redis holding the subscription/customer pairs, we can control ownership in record time with very little data. We should be ready for the release of the BFF for My Deliveries.