Securing an API Gateway with a JWT Authorizer using Cloud Formation.
Introduction
In my previous tutorial, I deployed a Cloud-Native app to AWS using services like Amplify for the front-end app and Copilot ECS for the backend. In the first part of the tutorial, I show you how to validate the app functionality (in terms of your local environment) against the backend services deployed on AWS. In the second part, I will show you how to host your app in the Amplify service. So now, you have a Native-Cloud application running on AWS. The missing piece in this scenario is that we didn’t validate the user session token in the API Gateway, and it’s the task for this new tutorial.
Remember that with Amplify CLI, we created a Cognito service integration into the app, so our users must signup first before using the application. When the users log into the app, Cognito generates a JSON Web Token (JWT) that we can use later to send to the API Gateway in every HTTP request. So our API Gateway can validate this session token against Cognito to verify if the user can access the Timer service API. We need to do this using our Cloud Formation (CF) template.
UPDATE: After you finish reading this article, I recommend you read a new tutorial that I wrote using the same Timer Service application described here but with a Quarkus OIDC configuration in the Java backend application to validate the user’s session token against Cognito as the second fence of validation.
To complete this guide, you’ll need:
NOTE: As usual, you will find the project’s source code used for this tutorial in my GitHub repository. You can download it to follow some configuration details shown in this tutorial.
IAM User Groups and Policies.
If you have followed some AWS tutorials on the web, you have noticed that many (if not all) use the “AWS System Administrator” policy to perform all the tutorial tasks. Conversely, I like to use specific IAM policies and roles to execute actions on AWS. The following image shows you the “IAM User Groups” that I’ve been using until now for the Timer Service:
My AWS user belongs to each of those groups inheriting the policies associated with them:
As shown in the previous image, all the assigned policies have the “Full Access” pattern. My intention here is not to go to the other extreme of setting more granular user policies. I want to do so, but it will be a tremendous task that I think isn’t necessary for the complexity of the different applications on the cloud. On the other hand, assigning administrative privileges to every user account you need isn’t a good idea.
Configuring the JWT Authorizer.
First, we need to create a JWT Authorizer. This resource now exists in our CF template for the API Gateway:
Then, execute the following command in the root of the Angular/Ionic directory to update the existing API Gateway on AWS:
# aws cloudformation update-stack \
--stack-name timerservice-apigateway \
--template-body file://cloudformation/1_ApiGatewayAuthorizer.yml \
--parameters \
ParameterKey=App,ParameterValue=timerservice \
ParameterKey=Env,ParameterValue=dev \
ParameterKey=Name,ParameterValue=ApiGateway \
ParameterKey=AppClientID,ParameterValue=abcdefgh1234567890 \
ParameterKey=UserPoolID,ParameterValue=us-east-xyz123 \
--capabilities CAPABILITY_NAMED_IAM
You must update the values written in bold with the corresponding ones. You can find those values in your CF console by selecting the stack created by the Amplify service and then clicking on the “Outputs” tab. You must see something similar to the following:
IMPORTANT: For the “App Client ID” parameter, you must select the value created by the CF service as “AppClientIDWeb.”
Once the CF command finishes the execution, you can verify the updated values in the API Gateway console:
As you can see, the API Route is now attached to our JWT Authorizer. In the “Manage authorizers” tab, you should know the Authorization details:
You can use the Postman tool to corroborate if the new configuration is working. You should see the following output:
Notice that the status code in the HTTP response is “HTTP 401 Unautorized”, which we spect because now we need to authenticate our users first before gaining access to the Timer Service API.
Now, run the Angular/Ionic app in your local environment and verify if the same behavior happens after you log in:
As you can see in the browser console, the same “HTTP 401 Unauthorised.” error is shown. To solve this problem, we need to create an Angular HTTP interceptor in our app. So firstly, we need to get the JWT token from the user session as it’s shown follows:
Secondly, the interceptor must put the user session token (JWT) in the header of every HTTP request. This header param is known as “Authorization”:
After this configuration is finished, we must now face another problem:
The browser security module makes a preflight request (an HTTP Options request) to our API Gateway to validate the CORS configuration. Then, it obtains an “HTTP 401 Unauthorised” error again. To solve this new problem, we need to delete the OPTIONS method from our original CORS configuration in the API Gateway and create a new API Route only for this method. So the new CORS configuration will allow preflight requests from the browser using an unauthenticated request that now must be handled by a Lamba Function.
Configuring CORS with a Lambda Function.
The original API Gateway has the following CORS configuration:
As we have configured the Authorization control in the API Gateway, we need to remove the OPTIONS method from the original CORS configuration and create a new one. We must create only an API Route and Integration resources for the OPTIONS method. This integration is responsible for calling a Lambda Function on every OPTIONS request, which now acts as a CORS configuration.
The API Route should look as follows:
Notice that I’m using only the OPTIONS method for this route. The API Gateway calls this new route every time it receives an HTTP OPTIONS request. This includes the preflight requests from the browser 😉.
The Lambda function that receives all the HTTP OPTIONS requests must have the following structure. I’m using NodeJS as the function code:
Finally, we need to update the API Gateway again using the CF file with your corresponding values (the AppCliendIDWeb and UserPoolD variables as before):
# aws cloudformation update-stack \
--stack-name timerservice-apigateway \
--template-body file://cloudformation/1_ApiGatewayAuthorizer.yml \
--parameters \
ParameterKey=App,ParameterValue=timerservice \
ParameterKey=Env,ParameterValue=dev \
ParameterKey=Name,ParameterValue=ApiGateway \
ParameterKey=AppClientID,ParameterValue=abcdefgh1234567890 \
ParameterKey=UserPoolID,ParameterValue=us-east-xyz123 \
--capabilities CAPABILITY_NAMED_IAM
Then, go to the API Gateway console on AWS and see that our newly created route doesn’t have an authorization rule attached. Only the “$default” route must have the authorization rule because that route is our communication channel between the API Gateway and the ECS Cluster:
The CORS configuration now must have the following parameters:
Now, it’s the time of truth. When you open the app again in your local environment, the error should not appear:
I put a logger in the interceptor service to print every HTTP request. You should see something like this:
As you can see in the previous image, the JWT token appears in the HTTP header before sending a request to the API Gateway on AWS.
Publishing the App to Amplify Hosting
When you push these changes to your GitHub repository, Amplify detects those changes and updates the app in the Hosting environment with your last changes. So, you can verify if those changes taking place on AWS by trying to access your app from the Amplify hosting:
After you log in into the app, you must see the same logs that you see in your local environment but this time, using your AWS account:
And that is!! Now you have your Native App running on AWS, but this time, your API Gateway allows secure connections only for your logged-in users.
Building Scripts
As you know, I like automating some of the procedures I made in my tutorials. So, in the main folder of this repo, you will find a script called “run-scripts.sh” that will show you an interactive menu with some of the essential steps that we made in this exercise:
# ./run-scripts.sh
You can find the details of these scripts in a folder called “scripts” in the project’s root directory. Those bash scripts execute CF templates that you can find in a folder called “cloudformation” in each of the projects for the backend or frontend, depending on the case.
That’s it for me until today. I hope this tutorial was helpful for you and your side projects.
Thanks for your reading!!.