Using Verified Permissions with Cognito to control access to API endpoints
Tags: aws, serverless, security, apigateway
Published: 2024-04-18 06:56:51

If we use Cognito User pools as an identity provider, AWS now enables us to configure fine-grained access control to our API Gateway endpoints using Amazon Verified Permissions.

## 1. A recent release

AWS has recently <a href="https://aws.amazon.com/about-aws/whats-new/2024/04/amazon-cognito-customers-access-apis-verified-permissions/" class="link" target="_blank" rel="noopener noreferrer">published</a> a new feature for <a href="https://docs.aws.amazon.com/cognito/latest/developerguide/what-is-amazon-cognito.html" class="link" target="_blank" rel="noopener noreferrer">Cognito</a>. The release introduces the use of <a href="https://docs.aws.amazon.com/verifiedpermissions/latest/userguide/what-is-avp.html" class="link" target="_blank" rel="noopener noreferrer">Amazon Verified Permissions (AVP)</a> to securely manage access to REST-type <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html" class="link" target="_blank" rel="noopener noreferrer">API Gateway</a> endpoints via a <a href="https://docs.aws.amazon.com/lambda/latest/dg/welcome.html" class="link" target="_blank" rel="noopener noreferrer">Lambda</a> authorizer.

## 2. The scenario

Bob was thrilled to explore this new feature and quickly implemented it in one of his company's applications. He had previously secured several endpoints using **scopes** and **access tokens**. When his manager requested the addition of a new endpoint, Bob chose to implement the new AVP-based access control mechanism.

In this post, I will compare two of their API endpoints: `GET /scope` and `GET /avp` (names have been changed for privacy :wink: ). Both endpoints use access tokens to make authorization decisions, but they are secured differently - through scopes and AVP, respectively.

## 3. Protecting API Gateway endpoints with Verified Permissions

Before diving into the comparison let's review the resources needed to implement this new access control mechanism with Cognito and AVP.

### 3.1. Using the wizard

Verified Permissions includes a new **wizard** that creates all necessary resources for this feature.

![Using the wizard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5bb6vsqekkjgj67jfcco.png)

The wizard guides us through several steps:

<ul class="list">
  <li class="list__item">Importing the API endpoints we wish to protect by selecting the API and its stage.</li>
  <li class="list__item">Choosing the Cognito user pool.</li>
  <li class="list__item">Assigning Cognito group users to specific endpoints.</li>
</ul>

The final step involves the wizard provisioning all the resources listed below. Currently, we need to manually **add the API Gateway authorizer** to the endpoints and then **deploy the API** if using the AWS console.

### 3.2. Verified Permissions resources

We'll need the following resources for AVP, all created via the console wizard.

**Policy store**

A container for policies, described with the API ID and stage included.

**Identity source**

Currently, the only supported identity provider is Cognito. We specify our <a href="https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html" class="link" target="_blank" rel="noopener noreferrer">Cognito user pool</a> here with `MyApi::User` as the principal type (`MyApi` is the policies' namespace). We choose whether to use identity or access tokens for authorization and can opt to authorize requests that match a specific app client ID.

**Schema**

This defines the supported actions and entity types. For example, `User` is the principal, `get /avp` is the action, and `Application` is the resource.

**Policies**

Policies are written in <a href="https://www.cedarpolicy.com/en" class="link" target="_blank" rel="noopener noreferrer">Cedar language</a> and determine whether to `ALLOW` or `DENY` requests similarly to IAM policies. An example policy looks like this:

```
permit(
  principal in MyApi::UserGroup::"IDENTITY_POOL_ID|avp-group",
  action in [ MyApi::Action::"get /avp" ],
  resource
);
```

This policy will allow users in the `avp-group` Cognito group to call the `GET /avp` endpoint in `MyApi`.

We can also create policies for individual Cognito users. In this case, we should specify the **Cognito ID** as `principal` in the policy:

```
permit(
  principal in MyApi::User::"IDENTITY_POOL_ID|USER_ID",
  action in [ MyApi::Action::"get /avp" ],
  resource
);
```

Although it allows us to control granular access at the user level, I would still use groups and add the users to them as it seems to provide less management overhead.

### 3.3. Cognito resources

We should create a **user pool** and several **groups**, such as the `avp-group` that contains certain users. This group acts as the `principal` element in the AVP policy.

### 3.4. Lambda authorizer

API Gateway calls this function when a request reaches the protected endpoint. The function sends the necessary data to the `IsAuthorizedWithToken` Verified Permissions endpoint for authorization.

Sample input payload:

```js
{
  accessToken: 'ACCESS_TOKEN',
  policyStoreId: 'POLICY_STORE_ID',
  action: { actionType: 'MyApi::Action', actionId: 'get /avp' },
  resource: { entityType: 'MyApi::Application', entityId: 'MyApi' }
}
```

`IsAuthorizedWithToken` will verify the token and make an authorization decision based on the payload and the available policies.

The response object (`avpResponse`) includes an `ALLOW` or `DENY` decision, based on the policies.

```js
const avpResponse = {
  '$metadata': {
    httpStatusCode: 200,
    attempts: 1,
    totalRetryDelay: 0
    // other properties
  },
  decision: 'ALLOW',
  determiningPolicies: [
    { policyId: 'POLICY_ID_1' },
    { policyId: 'POLICY_ID_2' }
  ],
  errors: [],
  principal: {
    entityType: 'MyApi::User',
    entityId: 'COGNITO_USER_POOL_ID|COGNITO_USER_ID'
  }
}
```

The `COGNITO_USER_ID` will match the `sub` claim of the access (or identity) token.

The Lambda function returns an **IAM policy object** reflecting this decision to API Gateway:

```js
{
  principalId: 'PRINCIPAL_ID',
  policyDocument: {
    Version: '2012-10-17',
    Statement: [
      {
        Action: 'execute-api:Invoke',
        Effect: avpResponse.decision.toUpperCase() === 'ALLOW'
          ? 'Allow'
          : 'Deny',
        Resource: 'RESOURCE'
      }
    ]
  },
}
```

API Gateway then allows or denies the request.

### 3.5. API Gateway authorizer

Finally, we need a **Lambda authorizer** resource linked to the function in API Gateway. This can be set up separately or through the wizard.

## 4. Comparing access control with tokens only and AVP

Now let's compare the traditional token-based API access control to the AVP method described above.

### 4.1. Access tokens

Access tokens embed authorization details in the `scope` claim, which API Gateway inherently recognizes. If the scope in the token matches what's defined in the API Gateway authorizer settings, the request is allowed to proceed; if not, it's automatically rejected.

However, this approach isn't without its challenges. It requires additional setup steps that can complicate the process.

Firstly, we must establish a resource server in Cognito and define the specific scopes you intend to use. Then, during user authentication - essentially when users log into your application - we must ensure that these required scopes are included in the request to the token endpoint. Failure to include these scopes means they won't appear in the access token, potentially blocking access where it's needed.

This method can become cumbersome if the system requires a variety of permissions. An alternative is to <a href="https://dev.to/aws-builders/using-customized-access-tokens-to-set-up-authorization-in-api-gateway-26go" class="link" target="_blank" rel="noopener noreferrer">implement</a> a **pre-token generation** function within Cognito. This function dynamically injects user-specific scopes into the access token based on membership in Cognito groups or according to rules defined in a database.

### 4.2. ID tokens

Identity tokens primarily store user information. API Gateway is capable of validating these ID tokens to confirm user authentication.

However, since ID tokens lack authorization data, additional measures are necessary to manage access control. We have two options: deploy a Lambda authorizer that assigns permissions based on user or group identity or handle the authorization logic at the backend. In either scenario, we should maintain an external map of users and their corresponding permissions to ensure secure and efficient access management.

### 4.3. Using AVP

As highlighted earlier, this new feature utilizes a Lambda authorizer. Currently, API Gateway does not directly support authorization using Verified Permissions.

Using AVP eliminates the administrative burden associated with managing scopes and custom authorizations. The Lambda authorizer employs standardized code that remains consistent across different users, groups, and policies, simplifying the setup process.

AVP enables detailed access control without the reliance on scopes. This method enhances scalability allowing quick integration of new Verified Permissions policies into the policy store. Configuring access for user groups to specific endpoints is straightforward—simply by making selections in the console. Moreover, there's no need for the pre-token generation Lambda functions previously required.

In my experience testing API Gateway with AVP authorization, I found it significantly simpler than managing scopes. If AWS integrates native support for Verified Permissions within API Gateway - eliminating the need for a separate Lambda authorizer - it would offer a significant improvement over traditional access control methods in API Gateway.

### 4.4. Access vs ID token

As noted earlier, when setting up resources for Verified Permissions, we must specify whether to use ID or access tokens.

Both token types can effectively manage access control since Cognito embeds group information into each token type's `cognito:groups` claim. However, it's important to note that this doesn't apply simultaneously.

Choosing an ID token during the setup means that attempting to use an access token later will result in an error:

```
ValidationException: Failed to verify IdentityToken: \
invalid token type: Expected IdentityToken in the IdentityToken \
parameter but received AccessToken.
```

The reverse scenario also holds true - if our configuration specifies an access token AVP will not accept an ID token. Therefore, we should make a clear decision about the token type from the start and consistently use that type for authorization.

## 5. Summary

We can now enhance the security of API Gateway endpoints using Cognito and Verified Permissions. The required resources can be efficiently provisioned through the wizard available on the Verified Permissions console page.

This new method of access control simplifies the developer’s task of setting up endpoint authorization by eliminating the need for configuring scopes and streamlining the authorization process.

## 6. Further reading

<a href="https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-authorization-with-avp.html" class="link" target="_blank" rel="noopener noreferrer">Authorization with Amazon Verified Permissions</a> - More examples and details on the feature

<a href="https://docs.aws.amazon.com/cognito/latest/developerguide/tutorial-create-user-pool.html" class="link" target="_blank" rel="noopener noreferrer">Create a new user pool</a> - What the page title says

<a href="https://docs.aws.amazon.com/verifiedpermissions/latest/apireference/API_IsAuthorizedWithToken.html" class="link" target="_blank" rel="noopener noreferrer">IsAuthorizedWithToken</a> - The IsAuthorizedWithToken endpoint specification

<a href="https://docs.aws.amazon.com/verifiedpermissions/latest/userguide/policy-stores.html" class="link" target="_blank" rel="noopener noreferrer">Amazon Verified Permissions policy stores</a> - Good title for this page

<a href="https://docs.aws.amazon.com/verifiedpermissions/latest/userguide/schema.html" class="link" target="_blank" rel="noopener noreferrer">Amazon Verified Permissions policy store schema</a> - Straightforward documentation page titles today

<a href="https://docs.aws.amazon.com/verifiedpermissions/latest/userguide/policies.html" class="link" target="_blank" rel="noopener noreferrer">Amazon Verified Permissions policies</a> - About AVP policies