AWS Cloud Operations Blog
Testing Amazon Cognito backed APIs using Amazon CloudWatch Synthetics
Customers who develop APIs can control access to them using Amazon Cognito user pools as an authorizer. Testing these APIs should take into account the additional security controls in place to effectively validate that the APIs are working, and Amazon CloudWatch Synthetics enables proactive testing of these APIs.
If you are using Amazon Cognito User Pools as an Authorizer with the App Client configured in confidential client mode and want to test the protected APIs, this blog post will guide you step-by-step on how to modify the source code of your canary. It will show you how to first authenticate against the Amazon Cognito user pool and then use the resulting token to call the API.
The steps below leverage the Amazon CloudWatch Synthetics canary with the API blueprint to monitor an API that is backed by an Amazon Cognito User Pool.
Solution Overview
For this blog post, we will create an Amazon API Gateway GET method which has an Amazon Cognito User pool Authorizer. The Client Secret created from the Cognito User Pool Client is stored in AWS Secrets Manager. We then create a CloudWatch Synthetics Canary that first retrieves the client secret from AWS Secrets Manager and uses this secret to authenticate against Amazon Cognito to get a JSON Web Token (JWT) at the /oauth2/token endpoint as the Token Endpoint documentation. The canary then uses the token to make a GET request to the protected API Gateway method.
The CloudWatch Synthetics Canary does the following steps:
- Gets the client secret by using the GetSecretValue API.
- Exchanges the client secret for a JWT token by making a POST request at the /oauth2/token endpoint.
- Uses the JWT token in the Authorization header to make a GET request to the API Gateway method.
Figure 1: Architecture diagram
Prerequisites
The script below automates pre-requisites setup for the environment needed to run the canary, reducing the need to spinz resources manually to test the authentication. To deploy the prerequisites, AWS CloudShell streamlines the process by providing a pre-configured environment with AWS CLI pre-installed. This simplifies the execution of infrastructure as code directly within the AWS Management Console. For this blog post, we recommend using CloudShell; however, you can also use your own command-line tools if you prefer.
To set up the necessary environment for running the canary, follow these steps:
- In your AWS Management Console, type CloudShell in the search bar
- Select CloudShell and wait for the console to open
- Once the command line is loaded, you should be able to a screen similar to the image below
Figure 2: CloudShell for command line access to AWS resources and tools directly from a browser
We recommend keeping a separate browser tab for the CloudShell as you keep the session while browsing between AWS Services to execute the steps below.
Solution Walkthrough
Setting up Cognito
Lets run deploy the script to provision the required resources:
First, the script below creates a user pool in Amazon Cognito which will be used as part of the server authentication. Then, it creates a domain for the user pool, allowing a unique URL for JWT token generation. Following this, it sets up a resource server within the user pool, defining scopes for accessing resources. Additionally, it creates a user pool client, generating client credentials for accessing the user pool, which will be used by the Canary script.
To deploy the Cognito part, modify the USER_POOL_DOMAIN
variable that is currently set to domain-domain
to a globally unique value and copy and paste the code below in your CloudShell.
Setting up API Gateway resource
Continuing, the script below sets up an API Gateway instance, creating a RESTful API for the application. It configures an authorizer for the API Gateway, linking it to the previously created Cognito user pool for authentication. The script further defines integration settings for the API Gateway, specifying a proxy to an external HTTP endpoint. The HTTP endpoint can be any valid endpoint since we are using the API Gateway solely as a mechanism for authentication.
Similar to the code snippet above, run the below commands from CloudShell
Setting up Secrets using AWS Secret Manager
The commands below create a secret in the Secrets Manager that contains the User Pool Client Secret, along with other variables which will be used by the canary. This creates the secret using the default AWS managed key for Secrets Manager (aws/secretsmanager). Click here if you would like to learn how to use a customer managed key for your secret.
Creating IAM Policy and Role for canary
An IAM policy to allow access to Secrets Manager resource is also required, which will contain information like client ID, API Gateway URL, user pool ID, domain, and scope. The script below creates two policies and a role, which will be used while creating the canary.
The JSON output produced by the command lines above also includes the Secret ARN, API Gateway URL and the role required for the the new canary, which will be utilized in the subsequent step.
Testing the API Gateway
Now that we have Amazon Cognito and the API Gateway stitched together, we need to test if the endpoint is requiring an authorization token. To test the endpoint, let’s use the api_gateway_url outputted in the code snipped above to open a page using a web browser.
If everything is correct, you should see the following output in the browser:
This Unauthorized message is expected since no token is passed in the Authorization header.
Create the CloudWatch Synthetics Canary to test the protected API
Now, let’s create the canary to authenticate using the secrets generated by the create-user-pool-client command used above. The canary will retrieve these secrets in order to generate the JWT token, which can then be used to authenticate against the API.
Follow the steps below to create the canary using the CloudWatch Synthetics
- In your AWS Management Console, type CloudWatch in the search bar
- Open the Synthetics Canaries menu of the CloudWatch console, which is under Application Signals
- Choose Create canary
- Select Inline Editor.
- Enter
cognito-protected-api
for the Name, make sure you use this name to match the role created above. - Under Script editor, choose syn-python-selenium-3.0 as the Runtime version
- Under Lambda handler enter
apiCanaryBlueprint.handler
Figure 3: Create Canary script editor
- Paste the following script into the editor. The script first retrieves the API Gateway URL, User Pool Domain, User Pool Client ID, User Pool Client Secret, and User Pool Scope from Secrets Manager. It uses the User Pool Domain, User Pool Client ID, User Pool Client Secret and User Pool Scope to make a POST request to the Amazon Cognito domain with the /oauth2/token path to get a JWT token. With this token, it then makes a GET request against the API Gateway URL, adding the token in the Authorization header with “Bearer <token>”.
- In the Environment Variables section, set the following environment variable
SECRET_ARN
using the secret_arn JSON output generated in the CloudShell as you took note above. Note that the keySECRET_ARN
must be in upper case. An example screenshot is provided below:
Figure 4: Canary environment variables
- In the Schedule tab, change the Run canary to 1 minute
- In the Access Permissions tab, choose Select an existing role
- Select the role SyntheticsCognitoBlogPostRole. This role has sufficient permission to access the Secret Manager as we created above, otherwise the canary would fail due to lack of permission. These role and policies were created above during the prerequisites step.
Figure 5: Select an existing role for the Canary
- Keep the rest of the configuration as default and then choose Create Canary
The canary may take 1-2 minutes to be created, and you should be able to see the list of canaries in your account.
Checking the results
As per the role attached above, this canary now has permission to gather a JSON containing information such as Cognito User Pool details and the API Gateway URL. The Python Script which you used to create the canary will then utilize this information for authentication and calling the API Gateway URL.
Let’s return to the cognito-protect-api page and verify the script’s status.
In line with the image below, the console shows the Latest run tab as Passed. This signifies the successful generation of a JWT token and authentication against the API Gateway endpoint established during the prerequisites above. As you tested above in your browser, just by opening the API Gateway URL directly, you received the Unauthorized error, however, in the canary it is now authenticated.
Keep in mind: if the canary is returning an error, make sure you created the canary using the syn-python-selenium-3.0 runtime instead of nodejs.
Figure 6: Canary run passed
The following code snippet demonstrates the use of the pool_manager.request_encode_body
method to send a request to the Amazon Cognito service and obtain an access token. This token serves as a credential that authorizes access to secured resources. Upon receiving the token, it is extracted from the response and stored.
Subsequently, the obtained token is utilized in the authorization header ('Authorization': f'Bearer {token}')
when making a request to an API Gateway (APIGW_URL).
If the call completes without any error, the canary passes and the test is completed. However, if an incorrect token is provided, the connection will fail.
Exploring canary logs
The canary executed without any issues, but if you want to delve a little deeper, click on Logs tab for the canary cognito-protected-api to see the output generated by the HTTP Endpoint used in the API Gateway steps above, which is http://petstore-demo-endpoint.execute-api.com/petstore/pets.
The canary log output should display a result similar to the image below, proving that the canary was able to connect to the API Gateway endpoint, which authorized the call using the API Gateway Authorizer before forwarding the call to the petstore demo endpoint.
Figure 7: Canary availability logs
Clean up
This section provides the necessary information for deleting various resources created as part of this post.
To clean up the resources created during this tutorial, follow these steps:
1. Deleting the canary
It is important to note that deleting a canary is irreversible and all associated data and settings will be lost. Make sure you have made all necessary backups or extracted all relevant data before proceeding with the deletion.
To delete a CloudWatch Synthetics canary, follow the Delete canary steps.
2. Deleting the API Gateway
Deleting the API Gateway cleans up the authorizor, method, stages and deployment without us having to do it. Using the CloudShell session created before, run the below command.
3. Deleting the Cognito User Pool
First remove deletion protection, delete the Cognito User Pool Domain, then delete the Cognito User Pool. Using the CloudShell session created before, run the command below.
4. Delete the Secret
Similar to the steps above, now remove the secret.
Conclusion
In this blog post, we explained how to test cognito protected API Gateway APIs that are configured for confidential clients. This involves using a CloudWatch Synthetics Canary to first retrieve the client secret using the Secrets Manager GetSecretValue API and then exchanging the client secret for a JWT token by making a POST request at the /oauth2/token endpoint. The resulting JWT token can be used in the Authorization header to make a GET request to the API Gateway method.