Service to Service authentication using OAuth2 for AWS Serverless stack
Lately, I have been spending some time learning about implementing micro services using AWS Serverless stack (AWS API Gateway, AWS Lambda and AWS Dynamo DB). I have also been trying to find out on what will be a good way to secure micro services with a specific wish list in mind:
- No security information is stored within the bounded context.
- Consumers can be added dynamically (without service restarts/reloads)
- Ability to control method level scopes (read vs writes) for consumers. Also, scopes can be change dynamically without any restarts/reloads.
- Causes minimal Cognitive friction. Easy to understand, test and implement.
- A central place to administer all APIs, Clients and permissions clients have on those APIs.
- (Optional) Can be used on a Serverless stack, Containers and VMs. Can be used on AWS, GCP, Azure and On-premise with equal ease.
After some Googling and Christmas reading, OAuth2 (Client credential grant to be specific) looked like it ticked all boxes. To see how everything fits together, I wanted to implement something. This post is about how I used a set of tools to achieve this.
Here is a talk on various options authentication options and why OAuth2 (Client credentials grant) is a good choice.
And here is the slide deck for the impatient (me included)
Auth0 vs AWS Cognito
For obvious reasons, I did’t want to install/operate my own OAuth2 Authorisation server.
As an AWS native, the first choice that came to mind was AWS Cognito. To my surprise, I did not find anything around non-interactive client authentication. Everything was about user authentication. I might be pointed to something at a later time by my AWS buddies but for now I decided to look at Auth0.
Auth0 seemed has all the bells and whistles I needed for
- Service to Service Auth using OAuth2 client credential grant
- Ability to specify scopes for APIs
- Fully managed service. Has a good Web-UI and rich APIs.
- Awesome documentation. The fact that I figured my way around in a few hours says something about their documentation.
In my research I also found something called “Plan B” ( http://planb.readthedocs.io/en/latest/service-to-service.html) which also has automated credential rotation. This could probably be implemented by a combination of Auth0 APIs, S3 and AWS KMS. Lets keep that discussion for another day. Okta and Ping identity might be the other options.
Nothing fancy as the focus is more on securing the service. I choose to create a simple CRUD service for contacts. This can be contacts on your phone, contacts on your bank accounts, your email contacts.
AWS API Gateway as Micro Gateway which is primarily used to expose HTTP endpoints. It can also provide securing using API keys, CORS and rate limiting.
Lambda functions : Separate lambda functions for Dynamo DB operations. A different Lambda function as custom authoriser.
Contacts DB: AWS Dynamo DB as No SQL data store
Read Service is only allowed to call
GET /contacts and
Create Service is allowed to do all operations.
Auth0 is being used as an OAuth2 Authorisation Server.
Not going to detail every step here because its documented very well in a 4 part series here.
Just going to detail some important security details.
Define new API
What signing algorithm? I am going to pick RS256 which implies we have 2 keys one public and one private (secret). Auth0 uses the secret key to generate the signature and the consumer of the JWT uses the public key to validate the signature. RS256 is more secure as only the holder of private key can sign tokens and we can rotate secret key easily.
What scopes? I have just created 2 scopes for this service
write:contacts. Read scope gives access to GetItem, Scan and Query Lambdas while Write gives access to Create and Update.
Define two new Non interactive Clients
Create two new Non-interactive clients : Read Service and Write Service
This is the place where you also set the JWT expiration times and allowed origins individually for each client. There are many other settings but I left all of them at defaults.
Configure client’s Access to the API
Configure scopes for Read Service
And configure scopes for Write Service
When a client calls the
/contacts API endpoint, API Gateway invokes the custom authoriser lambda which does the following:
- Get public key from Authorisation server, in this case Auth0.
- Decode the token presented by the client using public key.
- Check scopes within the JWT match the scope for the service.
For GET method the only change I need in the above code is to change the requested scope.
const requested_scope = ['read:contacts'];
This is not the best Node.js code I have written but it works.
- Deploy AWS API Gateway, including all Lambdas, custom authorisers and Dynamo DB.
2. Get tokens for Read and Write Service by calling
POST /oauth/token. Need client_id and client_secret to call this.
POST /contacts using Read token and see it fail.
POST /contacts using Create token and create a few entries in Dynamo DB.
GET /contacts using Read token.
6. Call read using expired JWT and see it fail.
- No security information is stored within the bounded context : So far nothing stored however this will change in case I have to maintain black listed JWTs.
- Consumers can be added dynamically (without service restarts/reloads): Although I used Auth0 web UI to create new clients, this can also be done through APIs.
- Ability to control method level scopes (read vs writes) for consumers. Also, client scopes can be change dynamically without any restarts/reloads: I can control access to clients at a scope level. I can change scopes for clients without any restarts/reloads. Only caveat is when scope is changed for a client, existing tokens with old scopes will continue to work until they expire. This can be fixed using Blacklisting JWTs and adding another check into the custom authoriser. I will detail this in another blog post later.
- Causes minimal Cognitive friction. Easy to understand, test and implement: OAuth2 is a well understood subject. There are many books/articles you can read on it. More importantly, mature libraries exist in most languages to help with the implementation.
- A central place to view all APIs and Clients and permissions each of the clients have on those APIs: Auth0 web UI/APIs fit the bill pretty well.
- (Optional) Can be used on a Serverless stack , Containers and VMs. Can be used on AWS, GCP, Azure and VMs with equal ease: Not only is this solution platform and cloud independent it also takes away any heavy lifting done by API Gateways.
- Publish working code to Github.
- Check Blacklisted JWTs as part of “Custom Authoriser”. Auth0 has a way of black listing JWTs. Just need to write a scheduled lambda function to get them periodically. The custom authoriser can then check if the JWT is blacklisted and deny a request accordingly.
- Hide AWS API Gateway within a VPC. AWS API Gateway is always publicly visible. Hoping for AWS to provide a solution soon.
- Encrypt/Decrypt secrets used in Lambda functions using KMS
If you have made it this far, thanks for reading.
Do you have other ways in which you think micro-services can be secured? Have you implemented something similar? Is there a more AWS native way of doing this?
Feel free to reach out to me for more details.
I will be publishing code very soon. I have used the Serverless framework( https://github.com/serverless/serverless) and Nodejs to put this together.