← Home

AWS - restricted, user-aware APIs with API Gateway and Cognito

My team is working on a mobile application which needs to call out to various backend APIs to store and retrieve data related to each user.

A common way of handling this scenario is that when the user logs in via the mobile app, the authentication service generates a token for the user which the mobile app then sends on to any APIs it calls to prove that the user is logged in.

We’re using AWS Cognito to handle the authentication and token generation processes - creating a “User Pool” which holds information about each user, such as their authentication credentials.

We chose Cognito rather than rolling our own system because it reduces our workload - for example, with Cognito, we don’t have to manage secrets ourselves such as a private RSA key used to sign tokens or have to salt, hash and store user passwords. Cognito prevents us from having access to this data, which is useful, because if we don’t have it, it can’t be taken from us. It’s Amazon’s responsibility to take care of the security of Cognito and we believe that they can do a good job of it.

Cognito also integrates well with other AWS services such as API Gateway.

API Gateway receives incoming HTTP requests and forwards them to other (backend) locations, optionally modifying the structure of the request, applying caching and throttling. It also allows access to APIs to be restricted by the use of API keys or, more usefully in this case, AWS Cognito.

So, how does it work in practice?

First, you need to create an Authorizer:

Then, make sure the Authorizer is actually applied to each resource (URL path) in your API that you want to protect:

Once you click the tiny “tick” and deploy your API, attempts to access the resources will result in a HTTP 401 error, and nothing will reach your backend service:

Once you’ve logged on via Cognito and received an idToken, you can use this to be granted access to the API by adding it to the Authorization header of your API requests.

At this point, your backend API will be hit and can respond. However, there’s a problem, “backend” APIs don’t automatically receive the Authorization header, so they’ve got no idea who’s calling them, just that API Gateway has allowed it.

Fortunately, API Gateway can be configured to pass the Authorization header to the backend service.

First, the API Gateway resource must have the Authorization header added in the “Method Request” section.

Then, in the “Integration Request” section, a mapping from the input Authorization header to an output Authorization header needs to be put in place:

To test everything, I configured API Gateway to use an ngrok.io endpoint as a backend and viewed the incoming requests, where I could see that the Authorization header was coming through correctly. It looks something like this.

GET / HTTP/1.1
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI8dXVpZF91c2VyX2lkPiIsImF1ZCI6Ii4uLiIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJldmVudF9pZCI6Ijx1dWlkPiIsInRva2VuX3VzZSI6ImlkIiwiYXV0aF90aW1lIjoxNTEzMTY5MDY2LCJpc3MiOiJodHRwczovL2NvZ25pdG8taWRwLmV1LXdlc3QtMi5hbWF6b25hd3MuY29tL2V1LXdlc3QtMl9wb29sX2lkIiwiY29nbml0bzp1c2VybmFtZSI6Ijx1dWlkX3VzZXJfaWQ-IiwiZXhwIjoxNTEzMTcyNjY2LCJpYXQiOjE1MTMxNjkwNjYsImVtYWlsIjoiPGVtYWlsPiJ9.9ywdi62LvgoRntmqERoynjCm9ygKqLPwQSBlWsI7ZjE
x-amzn-apigateway-api-id: id
Accept: application/json
User-Agent: AmazonAPIGateway_name
X-Amzn-Trace-Id: Root=1-5a312506-a72c08ef3a10edd4481c0441
Host: 6804bc51.ngrok.io
X-Forwarded-For: ipv4

So, what is that token, and how do we use it work out which user has sent the request?

By this point, we know that the token is valid since the Cognito Authorizer in API Gateway has already checked that for us (assuming that your backend API is only accessible via the API Gateway). Assuming you trust Cognito and the Cognito Authorizer, then the token doesn’t need to be validated, we can trust whatever’s in the token to be valid.

So what is in the token? The token itself is a JWT (JSON Web Token), it’s a common standard, so libraries exist for most platforms to decode them, and even if they didn’t, since we don’t have to handle the cryptography to validate the token, it’s pretty easy to handle parsing them manually.

So, all our backend API needs to do is collect the value of the Authorization header, decode the “claims” section of the JWT and use whichever fields it needs (just the email address and the userid in my case).

{
  "sub": "<uuid_user_id>",
  "aud": "...",
  "email_verified": true,
  "event_id": "<uuid>",
  "token_use": "id",
  "auth_time": 1513169066,
  "iss": "https://cognito-idp.eu-west-2.amazonaws.com/eu-west-2_pool_id",
  "cognito:username": "<uuid_user_id>",
  "exp": 1513172666,
  "iat": 1513169066,
  "email": "<email>"
}