Controlling access to IAM-protected API endpoints with Cognito groups
Tags: aws, serverless, security
Published: 2023-10-26 08:29:06

Adding authenticated users to Cognito groups in User pools is an easy way to assign AWS credentials. We can configure the roles' permissions policy to prevent non-elevated users from accessing privileged endpoints in the API Gateway.

## 1. The scenario

Say we have an internal application where users belong to different permission groups. Some can only read data, and others might write them, too.

I <a href="https://dev.to/aws-builders/using-cognito-groups-to-control-access-to-api-endpoints-346g" class="link" target="_blank" rel="noopener noreferrer">discussed a way</a> to control access to endpoints using JSON web tokens and a <a href="https://aws.amazon.com/lambda" class="link" target="_blank" rel="noopener noreferrer">Lambda</a> authorizer earlier.

In this post, I'll aim to eliminate the tokens and the authorizer. The goal is to write less code and delegate more responsibility to AWS services!

## 2. Solution overview

The post shows a simplified solution, but we can also apply the principles on a larger scale. We will have two users, Alice and Bob, and two endpoints, `GET /test` and `POST /test`.

We'll store the **application user** data in a Cognito <a href="https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html" class="link" target="_blank" rel="noopener noreferrer">user pool</a> and create two **groups** there: `FullAccess` and `ReadOnlyAccess`.

Alice will belong to the `FullAccess` group, meaning she can both read and write data. Bob is in `ReadOnlyAccess`, so he can only invoke `GET` endpoints but cannot access anything else.

Each group will have an <a href="https://aws.amazon.com/iam/" class="link" target="_blank" rel="noopener noreferrer">IAM</a> role assigned. The roles will allow read/write and read access to the members of the `FullAccess` and `ReadOnlyAccess` groups, respectively.

After the user has signed in, the application will call some endpoints in an <a href="https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-identity.html" class="link" target="_blank" rel="noopener noreferrer">identity pool</a>. You can read more on how this process works in <a href="https://dev.to/aws-builders/controlling-access-to-resources-with-cognito-groups-and-iam-roles-3g66" class="link" target="_blank" rel="noopener noreferrer">this post</a>. The identity pool then assumes the IAM role assigned to the Cognito group the corresponding user is in. The application code will then access AWS resources with the role's credentials.

In this scenario we protect the backend compute resources with an <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html" class="link" target="_blank" rel="noopener noreferrer">HTTP API</a> type of <a href="https://aws.amazon.com/api-gateway/" class="link" target="_blank" rel="noopener noreferrer">API Gateway</a>. We'll set up <a href="https://dev.to/aws-builders/an-easy-and-secure-way-to-protect-api-gateway-endpoints-3o8c" class="link" target="_blank" rel="noopener noreferrer">IAM authorization</a> at each route, which eliminates the need for tokens and custom authorizers. It's also the safest way to protect an endpoint since it delegates the authorization task to the robust IAM service.

## 3. Pre-requisites

This post won't explain how to create

<ul class="list">
  <li class="list__item">an HTTP API</li>
  <li class="list__item">integrations for API Gateway</li>
  <li class="list__item">a Cognito identity pool</li>
  <li class="list__item">a Cognito user pool with hosted UI, Cognito domain, and callback URL.</li>
</ul>

I'll provide some links at the end of the post that will help spin up these resources if needed.

## 4. Key steps

Let's see what the key steps are to implement the architecture.

### 4.1 Setting up the IAM permissions

We'll need two separate roles, one for the `FullAccess` group and another for `ReadOnlyAccess`.

The mutual setting of the two roles is the **trust policy**. In both cases, we must allow the **identity pool** to assume the role:

```json
{
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "cognito-identity.amazonaws.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "cognito-identity.amazonaws.com:aud": "IDENTITY_POOL_ID"
        }
      }
    }
  ]
}
```

However, the permissions policies will differ because users in different groups will not have the same access.

The `FullAccess` policy can look like this:

```json
{
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "execute-api:Invoke",
      "Resource": [
        "arn:aws:execute-api:eu-central-1:ACCOUNT_ID:API_ID/*/GET/test",
        "arn:aws:execute-api:eu-central-1:ACCOUNT_ID:API_ID/*/POST/test"
      ]
    }
  ]
}
```

This statement allows the identity to invoke both the `GET /test` and `POST /test` endpoints.

The `ReadOnlyAccess` group's permissions policy won't contain the statement about the `POST` endpoint because those users only have read access:

```json
{
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "execute-api:Invoke",
      "Resource": [
        "arn:aws:execute-api:eu-central-1:ACCOUNT_ID:API_ID/*/GET/test",
      ]
    }
  ]
}
```

We call the roles `TestApiFullAccess` and `TestApiReadAccess`.

### 4.2. Enabling the IAM authorizer

We can protect both REST and HTTP API endpoints with **IAM**. It's a great way to perform authorization in machine-to-machine communication (e.g., microservices), but we can also use it between the front end and the back end.

When we want to enable IAM authorization in HTTP APIs, we go to **Authorization**, select the route and method (e.g., `GET`), and attach the `IAM (built-in)` authorizer to the route. We can also do it <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-access-control-iam.html" class="link" target="_blank" rel="noopener noreferrer">programmatically</a> using the CLI.

### 4.3 Signing the request

When we call an AWS service API endpoint (e.g., API Gateway `Invoke`, SNS `Publish` or S3 `CreateBucket`), we must sign the request using <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html" class="link" target="_blank" rel="noopener noreferrer">Signature Version 4</a>. The <a href="https://aws.amazon.com/developer/tools/" class="link" target="_blank" rel="noopener noreferrer">SDKs and the CLI</a> will automatically do it on behalf of us using the credentials we provide. But when we protect a public API endpoint with **IAM**, we should build the logic to sign the request in the code.

We can use the `Signer` class <a href="https://aws.amazon.com/amplify/" class="link" target="_blank" rel="noopener noreferrer">Amplify</a> provides in a <a href="https://react.dev/" class="link" target="_blank" rel="noopener noreferrer">React</a> front-end application to sign the API requests whose targets are various the API Gateway endpoints.

```js
// signing the request
const roleCredentials = {
  access_key: credentials.accessKeyId,
  secret_key: credentials.secretAccessKey,
  session_token: credentials.sessionToken,
};
const service = {
  service: 'execute-api',
  region: 'eu-central-1',
};

async function callGetEndpoint() {
  const request = {
    method: 'GET',
    url: 'https://DOMAIN_NAME/test',
  };
  const signedRequest = Signer.sign(request, roleCredentials, service);

  try {
    const response = await axios(signedRequest);
    setApiResponse(response.data);
  } catch (error) {
    setApiResponse({ message: 'Not allowed!!' });
  }
}
```

The `sign` method (as the name suggests) will sign the request. It accepts three arguments: the **request** parameters, the IAM **credentials**, and the **service** configuration.

The `request` object will contain the `url` and `method` properties. We should add a stringified `data` object for `POST` requests where we have some payload in the request body.

`roleCredentials` contain the AWS credentials: access key ID, secret access key, and session token. These are available from the return value of the `fromCognitoIdentityPool` call the `@aws-sdk/credential-providers` package provides (see below).

The main point in the service configuration is that we must specify `execute-api` as `service` and the region.

The `callPostEndpoint` function is very similar except for some minor differences in the endpoint, method, and the extra payload.

## 5. Putting it all together

Let's put it together and see a simple working React code.

```jsx
import axios from 'axios';
import React, { useEffect } from 'react';
import '@aws-amplify/ui-react/styles.css';
import { withAuthenticator } from '@aws-amplify/ui-react';
import { Signer } from 'aws-amplify';
import { fromCognitoIdentityPool } from '@aws-sdk/credential-providers';

function App({ signOut, user }) {
  const [credentials, setCredentials] = React.useState({});
  const [apiResponse, setApiResponse] = React.useState({});

  // getting the ID token that contains the role's name and ARN
  const idToken = user.signInUserSession.idToken.jwtToken;
  // making the role's credentials available
  useEffect(() => {
    async function credentialsFromCognitoIdentityPool() {
      const credentials = await fromCognitoIdentityPool({
        clientConfig: { region: 'eu-central-1' },
        identityPoolId: 'IDENTITY_POOL_ID',
        logins: {
          'cognito-idp.eu-central-1.amazonaws.com/USER_POOL_ID':
            idToken,
        },
      })();
      setCredentials(credentials);
    }

    credentialsFromCognitoIdentityPool();
  }, []);

  // Insert here the request signing code snippet
  // for GET and POST endpoints from above

  return (
    <>
      <button onClick={callGetEndpoint}>Call GET API endpoint</button>
      <button onClick={callPostEndpoint}>Call POST API endpoint</button>
      <p>{apiResponse.message}</p>
    </>
  );
}

export default withAuthenticator(App);
```

The front-end application will submit signed requests to the corresponding API Gateway endpoints. The `idToken` will contain the role name and ARN that we have assigned to the user group in Cognito. By calling the `fromCognitoIdentityPool` method, we'll receive the credentials of the role assigned to the group the user is part of.

The signed request will include the credentials in the `Authorization` header. The API Gateway will then use IAM to check the signature for permissions that allow to invoke the endpoint.

This way, we managed to eliminate the use of tokens and Lambda authorizers. We use Cognito User pools, Cognito Identity Pools, and IAM authorization to control access to API Gateway endpoints.

## 6. Considerations

The above solution is **a** possible way to control access to API Gateway endpoints, but it's not **the** way or, at least, not the only way to do so. One should investigate their use case before applying any codes and principles above in production.

## 7. Summary

We can add users to groups in Cognito User pools that can have IAM roles assigned. The role's permissions policy is suitable for controlling access to AWS services, like API Gateway.

This way, we can protect API endpoints with IAM authorization and control which users can access which endpoints.

## 8. Further reading

<a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html" class="link" target="_blank" rel="noopener noreferrer">Working with HTTP APIs</a> - Official documentation about HTTP API

<a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html" class="link" target="_blank" rel="noopener noreferrer">Choosing between REST APIs and HTTP APIs</a> - Comparison with lots of tables

<a href="https://docs.aws.amazon.com/cognito/latest/developerguide/getting-started-with-cognito-user-pools.html" class="link" target="_blank" rel="noopener noreferrer">Getting started with user pools</a> - How to create a user pool with an app client

<a href="https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-user-groups.html" class="link" target="_blank" rel="noopener noreferrer">Adding groups to a user pool</a> - Straighforward title from AWS

<a href="https://docs.aws.amazon.com/cognito/latest/developerguide/tutorial-create-identity-pool.html" class="link" target="_blank" rel="noopener noreferrer">Tutorial: Creating an identity pool
</a> - How to create an identity pool