Securing API Gateway with the Cognito Authorizer
This tutorial references the code in the aws-connectedcar-dotnet-serverless repository. If you're new to this course, see the introduction for information about setting up your workstation and getting the sample code.
Next in our tutorials about API Gateway security mechanisms, we're going to look at the Customer API in the sample code and its use of AWS Cognito and the Cognito Authorizer. For background on Cognito, OAuth 2.0 and delegated auth generally, see the Working with Authorizers & Cognito lesson if you haven’t already. What we’ll focus on with this tutorial are the nuts and bolts of setting up Cognito and its authorizer.
Setting Up Cognito
There are three resources that you need to define for Cognito in CloudFormation: a user pool, a user pool app client, and a domain. Let’s go through these one at a time, looking at the services.yaml template for the SAM deployment (these are exactly the same in the OpenAPI deployment as well).
First, here’s the resource definition for the Cognito UserPool:
There aren’t too many properties explicitly set in this resource because we’re using a lot of default values. Looking at what is explicitly set in the code above, we have the AllowAdminCreateUserOnly property set to true, on line 27, which blocks users from self-registering for accounts. Similarly, the RecoveryMechanisms element on lines 29-31 prevents users from recovering their own accounts. Otherwise, all we have is a user-defined resource name on line 32.
It’s worth covering some of the defaults for this resource that define the user accounts, and which are not very self-explanatory. One such element is the UsernameConfiguration which is where you set the CaseSensitivity property. By default, this is set to true, meaning users have to login with the correct username or email case-sensitivity. If set to false, then any letter case can be used. Another is the UsernameAttributes element, which defines what the login credential will be. The default is a username, but this element can enable the use of email addresses or phone numbers. Lastly, the Schema element lets you define constraints for either built-in or custom user attributes.
An important point about these User Pool resource properties is that most cannot be changed once the User Pool has been created.
The next Cognito resource you need to define is the UserPoolClient, which is shown below:
The UserPool is the parent entity that contains what, in OAuth 2.0 terminology, are called the “app clients”. As we’ve seen, most of the properties for the UserPool pertain to the users, their credentials, their attributes, and how they sign-up and recover their accounts. The UserPoolClient defines what the sources for the users will be and how they will be authenticated for one or more applications.
The SupportedIdentityProviders element is where the UserPoolClient resource defines what the sources for the users are. Our sample code, as shown on line 52 above, specifies only the users in the UserPool through the “COGNITO” value. This element could, however, contain additional federated identity providers if they are added to the UserPool. Otherwise, all the other elements and properties shown above have values that are standard for the OAuth 2.0 “authorization code” flow. The ExplicitAuthFlows element is one that is Cognito-specific. The “ALLOW_ADMIN_USER_PASSWORD_AUTH” value shown on line 46 enables you to obtain an access token through programmatic authentication, alongside the web-based flow.
Finally, the sample code includes a definition for a UserPoolDomain resource:
The sample code deployment scripts generate a pseudo-random name value that’s passed to the templates as a parameter and is applied here. This value is used as a sub-domain for the regionally qualified Cognito domains that you use for the web-based login and for the OAuth 2.0 endpoints that Postman uses (for example) in the token validation process. Note that we’ll step through this in the labs, which will make this clear if you haven’t been exposed to Cognito or OAuth 2.0 before.
Configuring the Cognito Authorizer
As we saw in the Working with Authorizors & Cognito lesson, authorizers are special-purpose Lambdas that API Gateway uses to validate credentials and determine whether to allow or deny a request. In the next tutorial, we’ll show how to create and configure a custom Authorizer Lambda. For this tutorial, we're using the built-in Cognito Authorizer, which has to be declared and configured in either a SAM template for the API or an OpenAPI document.
Here’s the SAM template from the sample code where the CognitoAuthorizer is part of the Customer API resource:
Of interest, obviously, is the “Auth” element on lines 68-75. Line 69 specifies the CognitoAuthorizer that’s used; and lines 71-75 are the configuration for that authorizer. The values listed for the AuthorizationScopes are the OAuth 2.0 scopes that, for SAM deployments, are associated with all the endpoints for the parent API. Client applications will need to have at least one of these scopes present in their access tokens in order to be granted access to the endpoints.
The OpenAPI version of this Customer API resource doesn’t have any Auth element. The Authorizer is defined, instead, in the “securitySchemes” element of the OpenAPI document:
The format is different, but this is basically the same authorizer information that’s in the SAM template. Line 317 identifies the authorizer; line 320 indicates where the access token will be found in the requests; and lines 322-323 specify the OAuth 2.0 scopes that will be expected. Lines 324-328 are API Gateway extensions to the OpenAPI standard, which in this case identify the Cognito UserPool that’s used.
We saw that SAM templates apply the scopes automatically to all the endpoints. With OpenAPI these scopes have to be explicitly added to each endpoint, as shown below on lines 27-29:
Parsing Identity Claims in Lambdas
The code we’ve shown so far is for setting up Cognito and the Cognito Authorizer. There’s another piece of the puzzle, however, which is how the identity of an authenticated and authorized user is made available to the code inside the Lambdas at runtime.
In a nutshell, the access token that’s included with Cognito-secured API requests contains claims that can be used to identity the user. These claims are name/value pairs that are in the main section of the token. Some of these claims are always included, such as the “iss” claim that identifies the issuing Cognito User Pool, or the “client_id” claim that identifies the Cognito app client used. Other claims are for user attributes, when populated, from the User Pool.
In the Customer API for the sample code, the Lambdas need the “username” attribute from the claims in order to identify the user. How this is done is shown below, from the BaseRequestFunction.cs Lambda parent class in the sample code:
As you can see, the claims from the token are included in the APIGatewayProxyRequest that’s passed to the Lambda at runtime. A dictionary of these claims are extracted from the RequestContext on line 151, and the username is extracted from this dictionary on line 155.
Here’s an example Lambda event-handling method for the Customer API, showing how this username is then passed as an input to a service component that retrieves the customer’s profile: