Microservices API’s with serverless framework, AWS Lambda, API Gateway and Auth0 – Part2
Why Auth0?
Auth0 is a cloud-based identity & security management solution, that offer solutions ranging from universal login to single sign-on and user management. From my experience, these tend to be the hardest parts of an application to get right when we build new applications.
Auth0 gives you the ability to implement a wide range of Identity & Security management solutions much faster, by abstracting a lot of the complex stuff away. They also provide very developer-friendly APIs and some of the best documentation and sample code I have seen.
Solution Overview
Our mobile app and web clients will be authenticating against Auth0, and a Bearer token will be passed to AWS API Gateway. API Gateway will then make use of an AWS Lambda function to verify the token using Auth0. This will all be done within the custom authoriser framework that AWS currently provides.
Setting up Auth0
Head over to Auth0 and
Enter a name and an identifier for your API. Auth0 recommends using a URL, but don’t worry, the URL does not have to be publicly available and they will not call it. Note: the identifier will be used as the audience parameter as part of the oauth2 authentication call.
Once the API has been created, Auth0 automatically adds a machine-to-machine application, that we will be using for testing. You can also click on the test tab and look at the samples available for testing your new authentication provider.
Head over to the applications option on the left and have a look at the new application that has been added there.
In future, for each application that needs to authenticate, you will add a machine-to-machine application. Each application will have a unique Client ID and Client Secret.
Custom Authoriser Lambda Function
Auth0 has a great tutorial on how to connect AWS API Gateway to Auth0. I highly recommend reading through that. Here I am simply going to highlight the steps required to get our API secured using Auth0.
Auth0 has been nice enough to write the AWS custom authoriser Lambda function for us, you can download or clone the GIT repository here. Once downloaded or cloned, run the following command from within the project folder.
npm install
This will install all the NodeJs dependencies for the project.
Create AWS IAM Role
In order for our API to call the custom authoriser Lambda function, we need to configure an IAM role with the required permissions.
1 – Login to AWS and navigate to the IAM console. Click roles in the left-hand menu structure.
2 – Click create role
3 – Select AWS service as the type of trusted services.
4 – Select Lambda as the service that will be using the role.
5 – Click Next: Permissions
6 – Select the AWSLambdaRole permission from the list below
7 – Click Next: Review
8 – Provide a name for your role, like “Auth0Integration“
9 – Click Create role
10 – Click on your created role and select the Trust relationships tab and select Edit trust relationship
11 – Update your policy document to look like the below JSON file
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"apigateway.amazonaws.com",
"lambda.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
}
12 – Click update trust policy
13 – You will be directed back to the policy dashboard. You will see that the additional access has been added for API gateway. Also make a note of the policy ARN as you will need this a little later.
Deploying the custom authoriser Lambda function
Now that our policy has been created, we can deploy our custom Lambda function provided by Auth0. Navigate to the folder you downloaded earlier and run the following command
npm run bundle
This bundles our NodeJs application and produces a ZIP file ready for upload to AWS. Login to AWS and navigate to the Lambda console.
1 – Select create function. Use jwtRsaCustomAuthorizer as the function name.
2 – Selected node.js 8.10 as the runtime.
3 – Select “choose existing role” from the role section and select our previously created Auth0Integration role.
4 – Click Create Function.
5 – Scroll down to function code and select upload zip file from the code entry type and select the packaged (zip) file that was produced earlier on.
6 – With the package uploaded, we now have to configure some environment variables so the function knows how to call our Auth0 tenant and token service. The following environment variables are needed: TOKEN_ISSUER, JWKS_URI and AUDIENCE
7 – These values quickly be found by logging into your Auth0 account, and navigating to the machine-to-machine app that was automatically created earlier when we created the API.
8 – Click on settings and scroll down to the bottom and click on show advanced settings. Lastly, click on the endpoints tab. The TOKEN_ISSUER (OAuth Token URL without
6 – The audience value is the unique API value we specified as an URL earlier on. You can find the value by navigating to APIs, selecting your API and copying the Identifier value.
7 – Configure the AWS environment variables with the values above and click Save.
Testing our Lambda function
In the project we downloaded earlier, you will find a event.json.sample file with an input message we can use for testing. You will need to replace the “ACCESS_TOKEN” placeholder with an actual oauth2 token.
In the Auth0 console, navigate to your API and click the test tab. Use the CURL command (from your terminal) to authenticate and receive a token.
curl --request POST
--url https://xxxxxxxx.auth0.com/oauth/token
--header 'content-type: application/json'
--data '{"client_id":"8BsnXQA7saZKP1XFm21u05SGywvUEEdW","client_secret":"V0m0t53tcZrbSHGAWYDUWofH32EyVHTZkfxogCAipmM8EG3wt66JBrLG896DoTjP","audience":"https://my-awesome-api.com","grant_type":"client_credentials"}'
{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IlFUWkNSRU5GT1RrM05EUXdOak0wT1VVMFJqTTVNa1V3TXpRelFqSkRRVUpFTnpaRE5qTTVSZyJ9.eyJpc3MiOiJodHRwczovL2lkZWFsc29sdXRpb24uYXV0aDAuY29tLyIsInN1YiI6IjhCc25YUUE3c2FaS1AxWEZtMjF1MDVTR3l3dlVFRWRXQGNsaWVudHMiLCJhdWQiOiJodHRwczovL215LWF3ZXNvbWUtYXBpLmNvbSIsImlhdCI6MTUzODA2MDc3OSwiZXhwIjoxNTM4MTQ3MTc5LCJhenAiOiI4QnNuWFFBN3NhWktQMVhGbTIxdTA1U0d5d3ZVRUVkVyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.d2gvP0Vfm9Alii-OqsOLNc2ybK2BqDoKvpuDGwaz06iysdU4F-zKG5b8nMxix0uDXUXG0uGfLd9ZbrFq2THvNVibPMsF2hl8u5fLDUE5mXjBTU5XNteiWKP-ToXzsh-ZGBoaFJA3ZyswoUeAoaPxEasbsfsukfN2pc5iJ07kAwkHCWeloEt_wtnDhUiesWNN_ZzW_HvBiRRXbs0ZDUgnjefaNBkJMFTrM32a9n9SIMBY2TodojjJ_dcTSI_WAxRtK45D5qUn0Ulg7-SptWDCQSXGx_hlc8NN_5CPpAoyjOc0MsOwQjJBcq6nmhE_tHzBxhwCRctDdo78m80qPfHbuw","expires_in":86400,"token_type":"Bearer"}
Use the access_token above and replace the ACCESS_TOKEN variable in the sample message with the token.
{
"type" : "TOKEN",
"authorizationToken" : "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IlFUWkNSRU5GT1RrM05EUXdOak0wT1VVMFJqTTVNa1V3TXpRelFqSkRRVUpFTnpaRE5qTTVSZyJ9.eyJpc3MiOiJodHRwczovL2lkZWFsc29sdXRpb24uYXV0aDAuY29tLyIsInN1YiI6IjhCc25YUUE3c2FaS1AxWEZtMjF1MDVTR3l3dlVFRWRXQGNsaWVudHMiLCJhdWQiOiJodHRwczovL215LWF3ZXNvbWUtYXBpLmNvbSIsImlhdCI6MTUzODA2MDc3OSwiZXhwIjoxNTM4MTQ3MTc5LCJhenAiOiI4QnNuWFFBN3NhWktQMVhGbTIxdTA1U0d5d3ZVRUVkVyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.d2gvP0Vfm9Alii-OqsOLNc2ybK2BqDoKvpuDGwaz06iysdU4F-zKG5b8nMxix0uDXUXG0uGfLd9ZbrFq2THvNVibPMsF2hl8u5fLDUE5mXjBTU5XNteiWKP-ToXzsh-ZGBoaFJA3ZyswoUeAoaPxEasbsfsukfN2pc5iJ07kAwkHCWeloEt_wtnDhUiesWNN_ZzW_HvBiRRXbs0ZDUgnjefaNBkJMFTrM32a9n9SIMBY2TodojjJ_dcTSI_WAxRtK45D5qUn0Ulg7-SptWDCQSXGx_hlc8NN_5CPpAoyjOc0MsOwQjJBcq6nmhE_tHzBxhwCRctDdo78m80qPfHbuw",
"methodArn":"arn:aws:execute-api:us-east-1:1234567890:apiId/stage/method/resourcePath"
}
In AWS click on your function and select test. Create a test message with the sample message you created and execute the function. You should see a successful response like the one below.
Updating our API to use the custom authoriser
Lets open our severless.yml file and modify it to look like the one below
service: my-express-application
provider:
name: aws
runtime: nodejs8.10
stage: dev
region: us-east-1
functions:
app:
handler: index.handler
events:
- http:
path: /hello-world
method: get
authorizer: arn:aws:lambda:us-east-1:067239373928:function:jwtRsaCustomAuthorizer
We have added an authorizer property to the http method that points to our Lambda function ARN. You can find the function ARN by clicking on the function and looking in the top right corner.
sls deploy
If you copy the link produced by the deployment command into your browser and hit enter, you will notice that we receive an unauthorized exception back from the API. That is because we have not provided an OAuth token.
{"message":"Unauthorized"}
Testing with Postman
Postman is a very handy tool for testing REST services. It does what SOAP-UI does for SOAP-based web services.
Before we can test our service from Postman, we first need to get a new token from Auth0 again. Run the same CURL command from earlier and copy the token.
Now open Postman and create a new HTTP GET request with the URL of your API. Under the authorization tab, click Bearer token and paste your new token there. Call the service and you should now receive a successful response back.
Recap
So just to recap. We first created a new Auth0 account and tenant that will be used as our Authentication server. We then added an API that generates an audience value. The audience value is used by Auth0 to determine where requests needs to be routed to. The machine-to-machine application provided the client secret and id values also required as part of the OAuth 2 authentication call.
With Auth0 all setup, we configured and deployed our custom authoriser Lambda function. The function will be used by our API, to validate tokens first, before passing messages to our other Lambda function that implements the business logic (in this case, returns an Awesome message :))
Our serverless infrastructure-as-code (yml) file was updated to include the authoriser function and then deployed. The serverless framework then takes care of using AWS CloudFormation templates to manage resource creation and updates.
What’s next?
In the next part (part 3) of this series, I will cover the basic setup and configuration of Bitbucket pipelines to enable automated build and deploy upon code checkin. Provision will also be made for API stages and having different Lambda instances for each state.