Controlling access in service-to-service communications with Cognito - Part 2
Tags: aws, security, cognito, microservices
Published: 2022-05-19 06:50:11

We can quickly set up token validation in API Gateway using a Cognito User Pool authorizer. API Gateway natively integrates with Cognito, and we don't need to create any custom authorizer logic to control access to the endpoints.

This is the second (and last) part of the secure service-to-service communication with Cognito mini-series. If you haven't read Part 1, <a href="https://dev.to/aws-builders/controlling-access-in-service-to-service-communications-with-cognito-part-1-5dc1" class="link" target="_blank" rel="noopener noreferrer">consider skimming through the post</a> before continuing with this content.

## 1. The scenario - refresher

Bob has looked for a solution for authorizing requests between the microservices he and his team are developing. He decided to use <a href="https://aws.amazon.com/cognito/" class="link" target="_blank" rel="noopener noreferrer">Amazon Cognito</a> and the `client_credentials` grant type. This grant is specifically for controlling access in service-to-service communications and follows the <a href="https://www.oauth.com/" class="link" target="_blank" rel="noopener noreferrer">OAuth 2.0</a> standards.

Last week Bob managed to set up everything Cognito, and he was able to get the access token in <a href="https://www.postman.com/" class="link" target="_blank" rel="noopener noreferrer">Postman</a>.

He will now create a Cognito User Pool authorizer in <a href="https://aws.amazon.com/api-gateway/" class="link" target="_blank" rel="noopener noreferrer">API Gateway</a>. Bob will then add it to the endpoint he wants to protect, and will define the required permission (scope) for the token validation.

## 2. API Gateway settings

For simplicity, let's assume that Bob only has one endpoint in the API. After he has created more endpoints, he can follow the same steps to integrate API Gateway with Cognito for each endpoint. He could even use different authorization methods (for example, <https://dev.to/aws-builders/controlling-access-to-the-api-with-lambda-authorizers-5bbj" class="link" target="_blank" rel="noopener noreferrer">Lambda authorizers</a>) for the each endpoint.

### 2.1. Creating the authorizer

First, we need to create an **authorizer** in API Gateway. Let's call it `s2s-authorizer`.

![Creating the Cognito authorizer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ve9nmmtfvvmvadgd3kcg.png)

We have to select `Cognito` for Type and specify the user pool.

### 2.2. Protecting the /files endpoint

Next, we should go to the **Method Request** on the `GET /files` endpoint.

In the **Authorization** section, select the name of the Cognito authorizer (`s2s-authorizer`).

![Selecting the authorizer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fn4h6aetc44qnmuummuw.png)

After clicking the little tick icon, the API Gateway UI will automatically offer the **OAuth Scopes** section. We must specify the permission that API Gateway will look for in the access token. In this case, let the scope be `demo/read.file`.

![Adding the scope to the endpoint](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jp3zhkt083nrpqtf4ax1.png)

Our token will contain this scope (and `demo/write.file` too), so the request to the endpoint should be successful.

### 2.3. Testing the endpoint in Postman

We can now test the endpoint in Postman.

In the **Authorization** tab, we select `Bearer Token` and paste the access token we received from Cognito. Don't write `Bearer` in the box because Postman will convert it to `Bearer Bearer <TOKEN>`, and it won't work.

If we get the `Invoke URL` from **Stages** in API Gateway and call the `<INVOKE_URL>/files` endpoint with the access token in the header, we should get a successful response from the API.

## 3. axios call

Obviously, Bob won't use Postman to get the token and invoke the API. Instead, one application will call the other service, so he will have to implement the same logic in a <a href="https://aws.amazon.com/lambda/" class="link" target="_blank" rel="noopener noreferrer">Lambda</a> function.

The first application, which consists of the Lambda function, will invoke the URL of the API Gateway of the second service. For simplicity, we can create another Lambda function behind the endpoint. The endpoint handler can return some fake data. In reality, the integration will probably fetch some data from a database or perform business logic.

The caller service will use <a href="https://www.npmjs.com/package/axios" class="link" target="_blank" rel="noopener noreferrer">axios</a> to make the request.

### 3.1. Fetching the token

We must convert the steps from <a href="https://dev.to/aws-builders/controlling-access-in-service-to-service-communications-with-cognito-part-1-5dc1" class="link" target="_blank" rel="noopener noreferrer">Part 1</a> to get the access token from Cognito.

This part of the code can look like this:

```ts
interface CognitoClientCredentials {
  access_token: string;
  expires_in: number;
  token_type: string;
}

const getCredentials = async (): Promise<CognitoClientCredentials> => {
  const authUrl = `${COGNITO_DOMAIN}/oauth2/token`;
  const authRequestBody = new URLSearchParams({
    grant_type: "client_credentials",
  });

  const authParams = {
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Authorization: `Basic ${Buffer.from(
        `${APP_CLIENT_ID}:${APP_CLIENT_SECRET}`
      ).toString("base64")}`,
    },
  };

  const { data } = await axios.post<CognitoClientCredentials>(
    authUrl,
    authRequestBody,
    authParams
  );
  console.log("Successfully received access token");
  return data;
};
```

The `getCredentials` function fetches the token from Cognito. `COGNITO_DOMAIN`, `APP_CLIENT_ID`, and `APP_CLIENT_SECRET` are environment variables in the Lambda function, which will call the second service.

`Content-Type` must be `application/x-www-form-urlencoded`, so we must stringify the request body that contains the grant type.

### 3.2 Calling the other service

Now that we have the access token, we will place it in the `Authorization` header of the second request.

The code can be the following:

```ts
export const handler = async () => {
  try {
    const { access_token: accessToken } = await getCredentials();

    const { data: files } = await axios.get(FILES_SERVICE_URL, {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });

    console.log("Successfully received data: ", files);
    return files;
  } catch (error) {
    console.log("An error occurred", error);

    throw error;
  }
};
```

As we mentioned earlier, the access token, which contains the required scope (`demo/read.file`) API Gateway needs, will be in the `Authorization` header.

The request should succeed, and we can process the response.

## 4. Summary

API Gateway integrates with Cognito and validates access tokens based on their scopes. We can quickly set up a proof-of-concept for an internal service-to-service authorization pattern.

The key steps are to use `grant_type: client_credentials` to fetch the access token from Cognito and set up the required scope for the Cognito authorizer in API Gateway.

## 5. Further reading

<a href="https://github.com/arpadt/aws-architecture-samples/tree/master/service-to-service-authorization-with-cognito" class="link" target="_blank" rel="noopener noreferrer">Github repo</a> - The stack in <a href="https://aws.amazon.com/cdk/" class="link" target="_blank" rel="noopener noreferrer">CDK</a>

<a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html" class="link" target="_blank" rel="noopener noreferrer">Control access to a REST API using Amazon Cognito user pools as authorizer</a> - More info on the topic in the documentation