Authorization and Amazon Verified Permissions - A New Way to Manage Permissions Part VIII: Integration with Cognito
Tags: aws, avp, cognito, authorization
Published: 2023-08-03 15:43:44

Welcome back to my last blogpost of series on AWS Verified Permissions (AVP)! In the previous blogpost, we discussed the importance of [auditing and pricing](https://dev.to/aws-builders/authorization-and-amazon-verified-permissions-a-new-way-to-manage-permissions-part-vii-audit-and-pricing-2k5) in an enterprise-grade authorization system. Today, we're going to explore how to integrate AVP with AWS Cognito, a powerful identity service that can help you manage user identities and authentication in your applications.

## Integrating AVP with AWS Cognito
In any application, managing user identities and controlling their access to resources is a crucial aspect. Amazon Cognito is a service that helps you manage your users and their authentication. When integrated with AVP, Amazon Cognito can provide the identity context for your authorization policies. This means that you can use information from a user's Cognito identity, such as their user attributes, to make fine-grained access control decisions in AVP.

However, it's important to note that AVP doesn't currently process the `cognito:groups` claim, which means that Cognito integration can be used primarily for ABAC (Attribute-Based Access Control) rather than RBAC (Role-Based Access Control). The team is working on enabling support for group claims, which would allow for RBAC-type policies.

## Use Case: E-commerce Application
Let's consider an e-commerce application where we have Sellers. Each seller has a custom attribute called `discountPrivilege` which is a string indicating whether the seller has the privilege to give discounts or not.

### Creating the Schema
In AVP, we define the structure of the entities, actions, and context that the policy will use in a schema. For our use case, our schema will include a "Seller" entity type with a "custom:discountPrivilege" attribute, and a "Product" entity type. Our actions will include "Read" and "Discount".

Here's how our schema looks:

```bash
{
    "MyEcommerceApp": {
        "entityTypes": {
            "Product": {
                "shape": {
                    "type": "Record",
                    "attributes": {}
                }
            },
            "Seller": {
                "shape": {
                    "type": "Record",
                    "attributes": {
                        "custom": {
                            "attributes": {
                                "discountPrivilege": {
                                    "required": true,
                                    "type": "String"
                                }
                            },
                            "required": true,
                            "type": "Record"
                        }
                    }
                }
            }
        },
        "actions": {
            "Discount": {
                "appliesTo": {
                    "resourceTypes": [
                        "Product"
                    ],
                    "principalTypes": [
                        "Seller"
                    ],
                    "context": {
                        "type": "SellerContext"
                    }
                }
            },
            "Read": {
                "appliesTo": {
                    "resourceTypes": [
                        "Product"
                    ],
                    "context": {
                        "type": "SellerContext"
                    },
                    "principalTypes": [
                        "Seller"
                    ]
                }
            }
        },
        "commonTypes": {
            "SellerContext": {
                "attributes": {
                    "token": {
                        "attributes": {
                            "client_id": {
                                "type": "String"
                            }
                        },
                        "type": "Record"
                    }
                },
                "type": "Record"
            }
        }
    }
}
```

### Setting Up the Identity Source with Cognito
Before we can create our policy, we need to set up an identity source in AVP. The identity source is where AVP gets the identity context for authorization decisions. In our case, we're using Cognito as our identity source.

Here are the steps to set up a Cognito identity source in AVP:

- In the AVP console, navigate to "Identity sources" and click "Create identity source".
- For "Cognito user pool details" please select AWS Region and Cognito User Pool.
- For "Principal details" for "Principal type" select "MyEcommerceApp::Seller".
For "App client", enter the ID of the app client in your user pool that will be making requests to AVP.
For "Client application validation", select "Only accept tokens with matching client application IDs". This ensures that AVP only accepts tokens that were issued for the specified app client. Provide the "Client Application Id" (It's located in Cognito, we will cover it in the Testing section)
Click "Create identity source".


![Identity source](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2m1azjko7pp4meor5i3z.png)

Now it should looks like this:


![Created identity source](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/job36hclh7je2tphwkjl.png)


### Creating the Policy
Next, we define the rules for access control in a policy. Our policy allows a seller to apply discounts to a product when the seller's "custom:discountPrivilege" attribute is set to "agreed".

Here's how our policy looks:

```bash
permit (
    principal,
    action in [MyEcommerceApp::Action::"Discount"],
    resource
)
when { principal.custom.discountPrivilege == "agreed" };
```

## How It All Works Together

With our schema, identity source, and policy set up, here's how the authorization process works:

-  A `Seller` logs in through Cognito and receives an ID token. This token includes the seller's user attributes, including the `custom:discountPrivilege` attribute.
- The seller makes a request to our application to perform an action, such as applying a discount to a product. The request includes the seller's ID token.
- Our application sends the ID token to AVP along with the requested action and resource. This is done through the `IsAuthorizedWithToken` API operation.
- AVP validates the ID token and extracts the identity context, which includes the custom:discountPrivilege attribute.
- AVP evaluates the policy using the identity context, action, and resource. If the policy permits the action (i.e., if the custom:discountPrivilege attribute is set to "agreed"), AVP returns an "ALLOW" decision. Otherwise, it returns a "DENY" decision.
- Our application uses the decision from AVP to either allow or deny the seller's request.

This process allows us to make fine-grained access control decisions based on user attributes from Cognito. It's a powerful way to implement attribute-based access control (ABAC) in our application.

In the next section, we'll walk through how to test this setup to ensure everything is working as expected.

---

## Testing the Setup

This time we cannot use the Test Bench from AVP, we need to do it differently. We first need to create a Cognito user pool and an app client.
Here's a quick guide on how to do this:

1. **Create a Cognito User Pool**: In the AWS Management Console, navigate to the Cognito service and click on "Manage User Pools". Click "Create a user pool", give it a name, and click "Review defaults". On the next page, click "Create pool".


![User pool creation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ve44olnhaeuxswm4gfa6.png)



2. **Set Password Policy**: Make it default.

3. **Disable MFA**: In the "MFA and verifications" settings, set "Which second factors do you want to enable?" to "None". This is for simplicity in our testing setup; in a production environment, you would typically want to enable MFA for added security.

4. **Add Custom Attribute**: In the "Attributes" settings, click on "Add custom attribute". Set the attribute name to "discountPrivilege", the attribute type to "String", and check the "Mutable" box. This allows the attribute to be changed after the user is created.


![custom attribute](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/11icxrfe6eygt81qhfp2.png)



5. **Create an App Client**: In the "App clients" settings, click "Add an app client". Give it a name, uncheck all the "Auth Flows Configuration" options except for `ALLOW_ADMIN_USER_PASSWORD_AUTH` and `ALLOW_USER_PASSWORD_AUTH`, and click "Create app client".

![App client](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y41gvwd097zx7fi6xn86.png)


Now that we have our Cognito setup, we can create a user and assign them the `discountPrivilege` attribute. Here's how:

1. In the Cognito user pool, navigate to "Users and groups" and click "Create user". Fill in the required fields, set "Email verified" to "True", and in the "Custom attributes" section, set `discountPrivilege` to "agreed".

2. Note down the "App client id" from the "App client settings" in your user pool. We'll need this for testing (user pool as well).

## Testing flow with AWS CLI

With our Cognito setup ready, we can now test our AVP integration. Here's how:

Before we can test our setup, we need to authenticate our user and obtain an ID token (**Not the access token, as there will be not custom claims included**). When a user is created in Cognito, they are required to change their password after their first sign-in. This process involves initiating an authentication flow, responding to the new password challenge, and then authenticating again with the new password to get the ID token.

The command to do it is like follow:

```shell
aws cognito-idp admin-respond-to-auth-challenge \
    --user-pool-id user-pool-id \
    --client-id client-id \
    --challenge-name NEW_PASSWORD_REQUIRED \
    --challenge-responses USERNAME=username,NEW_PASSWORD='YourNewPassword' \
    --session "YourSessionString"
```


Here's how to do this with the AWS CLI:

**Initiate the Authentication Flow**: Use the `admin-initiate-auth` command to initiate the authentication flow. This command requires the user pool ID, the app client ID, and the user's username and password.

```bash
aws cognito-idp admin-initiate-auth \
    --user-pool-id <user_pool_id> \
    --client-id <client_id> \
    --auth-flow ADMIN_USER_PASSWORD_AUTH \
    --auth-parameters USERNAME=<username>,PASSWORD=<password>
```
Deal with `New Password Challenge`, and auth again. Then we should obtain the ID token, we will use it to test our AVP policy.

If you decode it with jwt.io it should looks like this:
```bash
"email_verified": true,
  "iss": "https://cognito-idp.eu-west-1.amazonaws.com/eu-west-user-pool",
  "cognito:username": "92d514d4-0071-70fa-2ab7-201b7f22167a",
  "custom:discountPrivilege": "agreed",
  "origin_jti": "e166c8de-3798-49be-9e39-0ddfe3055cda",
  "aud": "4bagaej4rb8m1t028lv5vsio8o",
  "event_id": "519e9b73-2231-4afc-b881-f748b9d6000e",
  "token_use": "id",
  "auth_time": 1691072849,
  "exp": 1691076449,
  "iat": 1691072849,
  "jti": "f33a5626-5f5d-45ce-8848-61a115bbdce7",
  "email": "example.com"
}
```

## Testing the AVP Policy
Now that we have our ID token, we can use the `is-authorized-with-token` command to test our AVP policy (make sure you have updated AWS CLI). This command requires the policy store ID, the ID token, the action, the resource, and the context.

```bash
aws verifiedpermissions is-authorized-with-token \
    --policy-store-id <policy_store_id> \
    --identity-token <id_token> \
    --action actionType="MyEcommerceApp::Action",actionId="Discount" \
    --resource entityType="MyEcommerceApp::Resource",entityId="Product" \
    --context '{"contextMap": {"custom.discountPrivilege": {"string": "agreed"}}}' \
    --region <region>
```

This command will return a decision ("ALLOW" or "DENY") and the policies that determined the decision.

```bash
{
    "decision": "ALLOW",
    "determiningPolicies": [
        {
            "policyId": "XBN1W6aDjwYdDC1HQyVGXX"
        }
    ],
    "errors": []
}
```

And that's it! You've successfully integrated AVP with AWS Cognito and tested your setup using the AWS CLI.

## Using avp-cli for the Scenario

The scenario we've discussed in this post is included in the [avp-cli](https://www.npmjs.com/package/avp-cli) tool. This command-line interface tool simplifies the process of creating and managing schemas, policies, and policy stores in AVP. However, to use the tool for this scenario, you need to have your Cognito setup ready as per the instructions provided in this post.

In the library there is also support for `isAuthorizedWithToken` so we can easily make an authorization request.

## Conclusion

In the next [blogpost](https://dev.to/aws-builders/authorization-and-amazon-verified-permissions-a-new-way-to-manage-permissions-part-ix-hierarchies-and-abac-8gm) we will focus on hierarchies. I'm also working on enhancing the [avp-cli](https://github.com/Pigius/avp-cli) to add more functionalities to it, so stay tuned for updates!