logo
Menu

Using Verified Permissions with Cognito to control access to API endpoints

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.

Published Apr 18, 2024

1. A recent release

AWS has recently published a new feature for Cognito. The release introduces the use of Amazon Verified Permissions (AVP) to securely manage access to REST-type API Gateway endpoints via a Lambda 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 😉). 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
Using the wizard
The wizard guides us through several steps:
  • Importing the API endpoints we wish to protect by selecting the API and its stage.
  • Choosing the Cognito user pool.
  • Assigning Cognito group users to specific endpoints.
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 Cognito user pool 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 Cedar language and determine whether to ALLOW or DENY requests similarly to IAM policies. An example policy looks like this:
1
2
3
4
5
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:
1
2
3
4
5
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:
1
2
3
4
5
6
{
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
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 implement 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:
1
2
3
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

Authorization with Amazon Verified Permissions - More examples and details on the feature
Create a new user pool - What the page title says
IsAuthorizedWithToken - The IsAuthorizedWithToken endpoint specification
Amazon Verified Permissions policy store schema - Straightforward documentation page titles today
Amazon Verified Permissions policies - About AVP policies
 

Comments