An independent guide to building modern software for serverless and native cloud

Working with Authorizers & Cognito

This lesson 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.

Once you’ve defined endpoints in API Gateway, the next step is to control access to them. API Gateway uses a flavour of Lambda called an “Authorizer” as the mechanism for controlling access to endpoints. What we’ll cover in this lesson is how Authorizers work, how to write custom Authorizers, and lastly, how you can use Cognito and its associated Cognito Authorizer with API Gateway.

Authorizer Basics

API Gateway Authorizers can work with credentials of any kind, typically located in the HTTP headers. They process incoming requests in the pipeline before validation, and before it’s proxied through to the intended target. This gives the Authorizer the opportunity to grant or deny access to the endpoint before the request goes any further.

Authorizers are implemented as Lambdas that are similar to the ones that handle API requests. The difference is that while Authorizers read the same APIGatewayProxyRequest inputs, they don’t return the corresponding APIGatewayProxyResponse outputs. Instead, they return an AuthPolicy response, which is a JSON data structure containing a PrincipalId and a PolicyDocument. The PrincipalId identifies the sender of the request and can be an application- or user-level identifier. The PolicyDocument is an IAM data structure that specifies multiple things, including:

  • an action that applies (“execute-api:Invoke”)
  • an effect (“Allow” or “Deny”)
  • a resource ARN to which the policy applies, which can be wildcarded so that the policy applies as narrowly or as broadly as required

The output from an Authorizer can optionally contain additional fields, which we don’t cover here. See the documentation for more information: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html

Writing a Custom Authorizer

The VehicleAuthorizer Lambda in the sample code is an example of a custom API Gateway authorizer. This authorizer calls a service component to validate vehicle VIN and PIN values that are sent in the HTTP headers. Based on the result of that call, it allows or denies access to all the endpoints and actions in the Vehicle API.

When a request is allowed, the output from this Authorizer looks like the example AuthPolicy response shown below. This example has a Resource property ARN that specifies the Vehicle API ID and the “api” stage, but wildcards all the applicable resource paths and HTTP actions:

{
  "principalId": "VIN12345678", 
  "policyDocument": {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Action": "execute-api:Invoke",
        "Effect": "Allow",
        "Resource": "arn:aws:execute-api:us-west-1:12345678:fxjir892/api/*/*”
      }
    ]
  }
}

When using a custom Authorizer for an API you also have the option to cache the results for the provided credentials. This is an important performance feature because with an Authorizer added to the request pipeline you are otherwise increasing the latency for every request.

Delegated Authentication

The downside of simple Authorizers like the one outlined above, from a security point of view, is that the users’ credentials have to be included in every request. These credentials are thus exposed to any reverse proxies that might be running between the application and the API, as well as to the API itself and all of its downstream Lambdas. Anyone with access to these services or their logs could potentially gain access to the included credentials.

A better approach is to integrate API Gateway with an identity service like Cognito. With Cognito, applications can delegate user authentication to a separate identity service, and depending on the authentication flow that’s used, they generally don’t have access to user credentials. Instead, what applications obtain from Cognito once users are authenticated are access tokens. These tokens contain limited information about their subjects along with a digital signature that enables consumers of the tokens, such as API Gateway, to verify the source and accuracy of their content.

Authenticating with the Authorization Code Flow

That’s delegated authentication with Cognito in very broad strokes. Now, let’s step through two example authentication flows to better understand how this works under the covers. The first is the OAuth 2.0-defined “Authorization Code Flow”, which is designed to enable web-based, delegated authentication in a way that doesn’t expose long-lived credentials in request URLs.

Here’s a diagram that expands on the one we saw earlier in this section illustrating this flow:

Here’s the breakdown for this sequence of steps:

Step 1: Redirect to Cognito

In order to use the application, a user clicks on a button or link to login, at which point they are redirected to the Cognito-hosted login page. The redirect to this page includes query parameters that specify the client ID and callback URL. These parameter values must correspond to those set in the Cognito app client that’s used for this application.

Step 2: Authenticate

Once on the login page, the user enters their credentials to authenticate. If this Cognito user pool has a trust relationship with a federated identity provider, and the app client is configured to include this provider, then this page will also include a link to this provider’s login page. Otherwise, the user authenticates on the Cognito login page.

Step 3: Perform Callback

If the user is successfully authenticated, then the Cognito login page will redirect to the provided callback URL for the application. This redirect will include what’s called an “Authorization Code” in the URL as a query parameter.

Step 4: Obtain Token

At this stage, the application callback page receives the Authorization Code in the URL from Cognito. Since this is a web-based flow, the code has to be returned to the application in this way, but it’s fundamentally not a secure way to send a credential. That’s why this code is short-lived. Now the callback page, through a secure, backchannel process, sends a POST request to the shown Cognito endpoint in order to exchange the temporary authorization code for a longer-lived access token. This request includes the client secret that’s defined for the app client, which is never shared publicly or transmitted in a URL. In response to this backchannel request Cognito returns an access token for the user.

Step 5: Redirect Back

Finally, after the user has been authenticated and the access token has been obtained through the callback process, the user is redirected back to the application. At this point the application has the access token, and for the remainder of the user’s session, it can include this token in requests to API Gateway.

Authenticating with the Cognito API

The Authorization Code Flow described above is a standards-based flow that, as was outlined at the start of this section, enables a variety of application integration scenarios. With Cognito you also have the option to use its API to perform user authentication. This option can be useful when you’re not running a web application in a browser, such as in mobile devices or when programmatically calling a Cognito-integrated API. You’ll see an example of the latter in the last section of this course where we demonstrate some API test automation that authenticates Cognito users from the command line.

One limitation to be aware of with authenticating through the Cognito API concerns OAuth 2.0 “scopes”. In a Cognito app client you can define application-level scopes that can be included in the tokens that are issued. These scopes can be used to differentiate access to API endpoints for different applications. When authenticating through the Cognito API, however, only the “aws.cognito.signin.user.admin” scope is included in the token. So, for an application that needs to support authentication through the Cognito API, the endpoints in API Gateway that will be called will also have to enable access for this specific scope (as the Customer API endpoints in the sample code do).

Using the Cognito Authorizer

Writing code for token validation, as you might for legacy API services, can be complicated. The validation process involves retrieving public keys for the specific Cognito User Pool, using these keys to validate the digital signature in the token, validating the expiry timestamp, and then potentially checking scopes in the token against those required for an endpoint.

But thankfully, API Gateway provides the built-in Cognito Authorizer that implements all this for you. The resource properties shown below for the CustomerAPI, on lines 70-75, are all that’s required to integrate a Cognito User Pool with API Gateway in a SAM template:

With this authorizer added to an API, all the steps described above for token validation are performed automatically. A valid, non-expired access token will be granted access to an endpoint, when so configured, provided the scopes required by the endpoint are present in the claims for the token.