Authorization and Amazon Verified Permissions - A New Way to Manage Permissions Part XIV: AVP Getting Started
Tags: aws, avp, authorization, cognito
Published: 2024-04-23 19:52:27

Welcome back to my blog post series dedicated to building authorization using Cedar and Amazon Verified Permissions. In a previous blog post we learned about usage of AVP with IaC [Cloudformation](https://dev.to/aws-builders/authorization-and-amazon-verified-permissions-a-new-way-to-manage-permissions-part-xiii-cloudformation-47d2). Today, we will cover the topic of recently added [AVP getting started](https://aws.amazon.com/about-aws/whats-new/2024/04/amazon-cognito-customers-access-apis-verified-permissions/).

## Learning curve
As with any adoption of any technology, there's always a learning curve, in the case of AVP, it's a new service, so, naturally, it's not popularized, and there aren't a lot of materials (but there are more and more every day), as well as a learning curve related to the Cedar language. 

The Cedar language is very simple and fun for the developer, but it is a learning curve if you want to implement it in an organization. Let's look at what we need to build authorization with AVP. 

![Learning Curve](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t76r5hd8u8g628y50xya.png)

To use AVP, we need several elements:

- **Policy store**, which is a container for our access policies, where all policies are kept, and every authorization request, is checked against a specific policy store.
- **Schema**, which is the definition of all entities for our authorization system, possible actions, hierarchies, required and optional attributes, and their types. With schema we can check that our access policies are correct against schema, and so is the payload we send to AVP.
- **Access policies** in Cedar language, which AVP then evaluates for us. 
- **payload** - once we have all of the above, it would be nice to finally test our policy store, but for that we need a payload that we will send to the AVP for authorization decision (i.e. we need to send information about the principal, the action it wants to perform, on what resource, pass the required attributes, hierarchy information, etc.)

When you look at it from the side, it seems like a lot of things, and it can be overwhelming. 

Rest assured, it was not clear to me at first either, but over time everything has been clarified, and now I fully understand it. That's why the avp-cli was created, to quickly help others understand what it's all about.

## Getting started
The AVP team was aware of a learning curve, so to simplify the use of AVP it added getting started to the service. As we know, most authorization cases are mainly RBAC, on API gateway, so the AVP team wanted to simplify the process (especially if you use Cognito for auth).


![Gateway flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/10rxnounjym3zvkhkkrk.png)

Typical flow as shown in the figure, Gateway with various endpoints, Cognito for authentication, in addition, we can have some Cognito authorizer that will check access to the gateway.

## What AVP Getting Started does for us:

- Allows for authorization setup through the AVP wizard in the console under API Gateway.
- Based on the actions in the API Gateway, it maps these actions to Cedar actions, eliminating the initial learning curve of Cedar.
- Creates a lambda authorizer that we can easily attach to the API Gateway and use, so we don't have to worry about how to build the payload for AVP and use it properly (and overall knowledge about lambda authorizer at all).
- On our part, deploying the gateway is required once the authorizer is attached.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sprtc7ifncykfs0tcxyg.png)



Let's assume that we would like to streamline basic flow, and add Role Base Access control (RBAC) authorization, based on groups in Cognito. Before, it was not possible to use Cognito groups in AVP, now it is possible.

So that it's not just a theory, we'll try using Getting Started on some simple examples.

## What we will build today?

Based on the AWS announcement post, consider the example of a loan application with two key actions:

- Creating a `Loan Request (POST /loan)`: Users can submit new loan applications.
- `Approving a Loan (POST /loan/approve/{loan_id}`): Certain users can approve loans that have been submitted.


Suppose we have two Amazon Cognito groups that have user permissions as follows:

`loan_creators`: Members of this group are allowed to perform the "Creating a Loan Request" action.
`loan_officers`: Members of this group have permissions for both creating and approve loans.

And based on that we would like to do authorization at the Gateway level.

## Starting point Gateway and Cognito

For the purposes of the blog post, I created two Cloudformation so that everyone can play with it themselves, I know how annoying it always is when you do not have an example at hand that you can quickly use in the console, so I prepared:

- [a simple Cloudformation with Gateway API](https://github.com/Pigius/awesome-amazon-verified-permissions-and-cedar/blob/main/cloudformation/avp-getting-started/api.yaml), which implements the two endpoints mentioned before.
- both endpoints are mocks (to make it simpler).
- a [simple Cloudformation for Cognito](https://github.com/Pigius/awesome-amazon-verified-permissions-and-cedar/blob/main/cloudformation/avp-getting-started/authentication.yaml) (with groups inside).

Feel free to do deployment of the above cloud formation templates in the AWS console (just grab the template, and go via Cloudformation wizard to deploy it).

![CF success](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ectfxrny0riplxqd8h0y.png)

## Gateway

![Gateway](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/awillnp0jm7wlly6iggk.png)

After successful deployment, you can see Gateway with two endpoints.

## Cognito 

After successful deployment, you can see the Cognito user pool with two groups, and the `avp-client` user pool client.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qian46nffsziinqn82hl.png)

At this stage, we don't have any users, so it would be worthwhile to create a user and add them to a group. This is relatively simple; you need to create a new user and add them to the appropriate group.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o34snepabbc6142e3ezs.png)

Now, let's finally have some fun with AVP! We have everything we need—authentication, gateway, groups, users in groups—so we can finally add authorization.

## AVP time!

### Gateway actions

Currently, there is no support for CloudFormation, so we need to do this manually. Open the AWS console, and then go to Amazon Verified Permissions.

Start by creating a new policy store, and then select the option `Set up with Cognito and API Gateway - new.`


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w73w39myae2pp0bklnjs.png)


First, we need to select an API; we choose our Loan API, select the stage (v0), and then press 'Import API'. Our resources from the API will automatically be translated into Cedar policies.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ehp4tf9iz3h7ynpohe14.png)


## Identity Source
Now, we need to use our Cognito user pool as an identity source. Simply select the newly deployed Cognito user pool, and use all default options untouched. 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pd4hp5y3mob1qszk3or5.png)

## Assign actions to groups

Now, we need to assign actions to our Cognito groups, do it the same way as in the picture below (based on our requirements).


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ilejqlbl0ajuvcb09k7i.png)

## Deploy policy store

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v418zxxl8gburca2j7ux.png)

For the last step, you need to hit `Create policy store` which will trigger the deployment of the Lambda Authorizer. 

You will need to wait for the deployment, and then make sure the lambda authorizer is attached to the gateway, and then you **need to re-deploy** API.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sfjbn5ryt12pf2gw80zh.png)

## Show me AVP!

 We can immediately check in practice how it works, but before we test it, it will be simpler to see exactly what we have generated in AVP.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1alqfy4uhbx16krbwkus.png)

Let's start from schema:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sx8gq3t6hdgj1hjnu3fz.png)

So we have two actions (our endpoints), principal as `User` and Resource as `Application`

As we deal with hierarchy, we also have `UserGroup`, which our `User` is a member of. So based on that, we have an access policy for group membership.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f8bk7qm62ttb8x7s3qmx.png)

Let's check the generated actions:

1. Policy defining permissions for loan_officers cognito group

```json
permit(
  principal in loan_api::UserGroup::"eu-west-1_sXVC4jUDf|loan_officers",
  action in [ loan_api::Action::"post /loan/approve/{loan_id}", loan_api::Action::"post /loan" ],
  resource
  );
```
2. Policy defining permissions for loan_creators cognito group	

```json
permit (
    principal in loan_api::UserGroup::"eu-west-1_sXVC4jUDf|loan_creators",
    action in [loan_api::Action::"post /loan"],
    resource
);
```

By specifying `loan_api::UserGroup::"eu-west-1_sXVC4jUDf|loan_officers` and `loan_api::UserGroup::"eu-west-1_sXVC4jUDf|loan_creators`, the policies are targeting actions based on the membership of the principal in these particular groups (with usage of `in` operator). They also explicitly define what actions these principals can perform. In the case of `loan_officers`, the policy permits actions like `post /loan/approve/{loan_id}` and `post /loan`. For `loan_creators`, the policy permits the action `post /loan`.

With deny as a default rule, we don't need to create `forbid` policies.

## Lambda

The generated lambda authorizer is a type of `request-based` authorizer, which also will have in the event data section, the data from the request, including query params etc.

The runtime is Node.

Lambda is quite simple, but for us, the most important part is:

```javascript
  let bearerToken =
    event.headers?.Authorization || event.headers?.authorization;
  if (bearerToken?.toLowerCase().startsWith("bearer ")) {
    // per https://www.rfc-editor.org/rfc/rfc6750#section-2.1 "Authorization" header should contain:
    //  "Bearer" 1*SP b64token
    // however, match behavior of COGNITO_USER_POOLS authorizer allowing "Bearer" to be optional
    bearerToken = bearerToken.split(" ")[1];
  }
  try {
    const parsedToken = JSON.parse(
      Buffer.from(bearerToken.split(".")[1], "base64").toString()
    );
    const actionId = `${event.requestContext.httpMethod.toLowerCase()} ${
      event.requestContext.resourcePath
    }`;

    const input = {
      [tokenType]: bearerToken,
      policyStoreId: policyStoreId,
      action: {
        actionType: actionType,
        actionId: actionId,
      },
      resource: {
        entityType: resourceType,
        entityId: resourceId,
      },
      context: getContextMap(event),
    };

    const authResponse = await verifiedpermissions.isAuthorizedWithToken(input);
    console.log("Decision from AVP:", authResponse.decision);
```
We can observe the building of the payload for AVP, which uses the token from Cognito. AVP will decode the token and validate the JWKS for us, so we don't need to do this in Lambda. Then, we use the `isAuthorized` action which performs the authorization request. Based on the decision, we either grant or deny access to the endpoint.

The full lambda code can be found [here](https://github.com/Pigius/awesome-amazon-verified-permissions-and-cedar/blob/main/cloudformation/avp-getting-started/lambda_handler.js). Please keep in mind that Lambda code could change, I am not the author of this code!

You don't need to explicitly create a log group for lambda, they will go to `/aws/lambda/yourFunctionName`.



## Let's test it in practice

Now that we have everything, we can finally test our solution. First, we need an access token from Cognito for our user. I recommend creating two users who are in two different groups.

Remember, when you create a new user, you must generate a new password for them, which can be done in the AWS CLI with:

```shell
aws cognito-idp admin-respond-to-auth-challenge 
```
You can generate code via AWS-CLI, SDK, or by doing the HTTP request via POSTMAN or curl like this:

```shell
curl --location 'https://cognito-idp.<REGION>.amazonaws.com/<USER_POOL_HERE!!!!!>' \
--header 'Content-Type: application/x-amz-json-1.1' \
--header 'Accept: */*' \
--header 'X-Amz-Target: AWSCognitoIdentityProviderService.InitiateAuth' \
--data-raw '{
  "AuthFlow": "USER_PASSWORD_AUTH",
  "ClientId": "<ID_HERE>",
  "AuthParameters": {
    "USERNAME": "<USERNAME>",
    "PASSWORD": "<PASSWORD>"
  }
}
'
```

As a response, you will obtain both an access token and an id token, use the access token.

If you decode the token with jwt.io you will see:

```JSON
{
  "sub": "sub",
  "cognito:groups": [
    "loan_creators"
  ],
  "iss": 'BLABLA",
  ...
  "auth_time": 1713899648,
  "exp": 1713903248,
  "iat": 1713899648,`
  "username": "daniel"
}
```
so you can double check at this point, whether a proper group is within the JWT of a user.

## Testing Gateway
Now we can test our endpoints, starting from the `loan approval endpoint`:

You can use this curl:

```shell
curl --location 'https://<gateway-id>.execute-api.eu-west-1.amazonaws.com/v0/loan/approve/1234' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <TOKEN GOES HERE>' \
--data '{"statusCode": 200}'
```

The `'{"statusCode": 200}'` is needed as is the mock integration type.

The same for `loan creation`:

```shell
curl --location 'https://<gateway-id>.execute-api.eu-west-1.amazonaws.com/v0/loan/' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <JWT here>' \
--data '{"statusCode": 200}'
```

If your user will be with a not allowed group you will obtain the below response:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2dfnphfr5kudmwzk9x2l.png)

## Summary, next steps

And that wraps up today's blog post. As you can see, you can quite simply "click through" the authorization setup for the API Gateway, which as a `getting started` seems sufficient for a PoC and taking the first steps.

When I started, such tools weren’t available, so there was a lot to figure out on your own.

It's also important to note that there is no CloudFormation support for this, so you can't just throw this into your CI/CD and expect it to automatically build. However, you do have something to start with, something you can copy, play with, and then start to build authorization flows in your organization.

You might also try expanding this example to better understand how the service works, for example, try validating an additional parameter from the query params, and learn what needs to be changed.

and now... **Go Build!**

In the next blogpost we will cover [AVP with Cognito Groups](https://dev.to/aws-heroes/authorization-and-amazon-verified-permissions-a-new-way-to-manage-permissions-part-xv-avp-with-cognito-groups-4gjm)


## Useful resources
- [Official quick overview and demo video](https://www.youtube.com/watch?v=OBrSrzfuWhQ)
- [Offical Workshop](https://catalog.us-east-1.prod.workshops.aws/workshops/e25284b6-851c-4a89-bee1-ed7571787895/en-US)