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:

  1. No security information is stored within the bounded context.
  2. Consumers can be added dynamically (without service restarts/reloads)
  3. Ability to control method level scopes (read vs writes) for consumers. Also, scopes can be change dynamically without any restarts/reloads.
  4. Causes minimal Cognitive friction. Easy to understand, test and implement.
  5. A central place to administer all APIs, Clients and permissions clients have on those APIs.
  6. (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

  1. Service to Service Auth using OAuth2 client credential grant
  2. Ability to specify scopes for APIs
  3. Fully managed service. Has a good Web-UI and rich APIs.
  4. 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.

Architecture

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.

Bounded Context

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

Client Services

Read Service is only allowed to call GET /contacts and GET /contacts/id.

Create Service is allowed to do all operations.

Authorisation Server

Auth0 is being used as an OAuth2 Authorisation Server.

Auth0 Configuration

Not going to detail every step here because its documented very well in a 4 part series here.

https://auth0.com/docs/architecture-scenarios/application/server-api/part-1

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 read:contactsand 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

Custom authoriser

When a client calls the /contacts API endpoint, API Gateway invokes the custom authoriser lambda which does the following:

  1. Get public key from Authorisation server, in this case Auth0.
  2. Decode the token presented by the client using public key.
  3. 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.

Demo

  1. Deploy AWS API Gateway, including all Lambdas, custom authorisers and Dynamo DB.
Successfully deployed stack

2. Get tokens for Read and Write Service by calling POST /oauth/token. Need client_id and client_secret to call this.

POST /oauth/token
Response

3. Call POST /contacts using Read token and see it fail.

Cloudwatch logs for POST Custom Authoriser. Scopes don’t match

4. Call POST /contacts using Create token and create a few entries in Dynamo DB.

Very simple dynamo db table only containing names as partition keys
Cloudwatch logs for POST Custom Authoriser. Scopes match this time

5. GET /contacts using Read token.

[
{
"checked": false,
"createdAt": 1515030182572,
"name": "Shaun",
"updatedAt": 1515030182572
},
{
"checked": false,
"createdAt": 1515030195092,
"name": "John",
"updatedAt": 1515030195092
},
{
"checked": false,
"createdAt": 1515030171072,
"name": "Gaurav",
"updatedAt": 1515030171072
}
]
Cloudwatch logs for GET Custom Authoriser. Scopes match this time

6. Call read using expired JWT and see it fail.

Cloudwatch logs for POST Custom Authoriser. Expired JWT

Wishlist Check

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. (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.

Todo

  1. Publish working code to Github.
  2. 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.
  3. Hide AWS API Gateway within a VPC. AWS API Gateway is always publicly visible. Hoping for AWS to provide a solution soon.
  4. 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.