Earlier this month, I happened to be in a technical discussion with a small team wherein I was being briefed about how authorization works in their microservice-based system.
Here's what a request to one of their protected resources looks like:
Though I've only depicted one
Protected Service, many of those exist. Also notice that there is no API gateway.
The responsibility of the
Auth Service in the figure is to:
- Generate Tokens on successful logins
- Validate Tokens
- Handle other Auth flows
This post is about point 2: should the Auth Service validate the tokens or should it be done by the individual services themselves?
Letting the auth-service validate tokens
Let's look at the pros and cons for this approach. Pros first:
- Easy to implement. Easy to maintain.
- The auth-service is responsible for all things auth. Since the implementations of token-generation and token-validation are generally related, it makes sense to keep them in one place.
Making the auth service perform token validation comes with it's own drawbacks that need to be considered too:
- Every microservice that needs authorization has a dependency on the Auth Service.
- The amount of load on the auth-service by every other protected service + the interservice communication overhead for a lightweight task of token validation is not always justified.
- The Auth Service is a single point of failure. If it fails, the dependent services are going to fail too.
Validating at every protected service
In this approach, the services that require authorization should validate the tokens. The protected service doesn't have to talk to the
Auth Service at all.
Now this does gives us some advantages over validating at the auth-service:
Protected Services are better isolated from the
- The single point of failure has been eliminated.
Now this might not always be possible, so let's look at some of the possible scenarios:
- Opaque Guid-style tokens
- Generated tokens are stored in a datastore.
- This datastore is queried each time to check if the token is valid.
- Tokens are revoked by removing it from the datastore.
- Bearer tokens
- The principal claims are part of the token.
- No datastore query required to validate the tokens.
- Cannot be revoked. Has a short TTL.
- Revocable bearer tokens
- Same as bearer tokens except that a custom revocation logic now exists which requires querying the datastore.
Considerations while validating tokens at each service
- Guid-style tokens and revocable bearer tokens will still require some minimal amount of communication between the
Protected Serviceand the
- Reducing inter-service communication always results in some kind of redundancy. In this case it's going to be redundant implementations of the token validation logic in every
Protected Services' implementations (as opposed to the services themselves) will be a tad more coupled with the
Auth Service. New iterations of the
Auth Serviceneed to be backward compatible with old implementations of the validation logic. This is especially true when using revocable bearer tokens.
Communication with the auth-service will be required whenever a datastore access is involved (since the AuthService owns its datastore). This can be reduced however by caching the required data for example.
The hybrid approach
A hybrid approach can be used to address the last two considerations listed previously. The idea is to let the service handle token validation and use the auth service as a fallback mechanism for token validation.
Protected Service's validation implementation could be put behind a configuration toggle and disabled to forcefully use the fallback mechanism if required.
While it's okay to use the
Auth Service for validation when starting out, I think it makes more sense to prefer validation at the individual protected services whenever possible.
At the end of the day, like many things in engineering, this too is about tradeoffs.