DEV Community

Cover image for Authorization and Amazon Verified Permissions - A New Way to Manage Permissions Part XIV: AVP Getting Started
Daniel Aniszkiewicz for AWS Heroes

Posted on • Updated on

Authorization and Amazon Verified Permissions - A New Way to Manage Permissions Part XIV: AVP Getting Started

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. Today, we will cover the topic of recently added AVP getting started.

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

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

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

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:

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

Gateway

Gateway

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

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

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

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

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

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

Deploy policy store

Image description

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

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

Let's start from schema:

Image description

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

Let's check the generated actions:

  1. Policy defining permissions for loan_officers cognito group
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
  );
Enter fullscreen mode Exit fullscreen mode
  1. Policy defining permissions for loan_creators cognito group
permit (
    principal in loan_api::UserGroup::"eu-west-1_sXVC4jUDf|loan_creators",
    action in [loan_api::Action::"post /loan"],
    resource
);
Enter fullscreen mode Exit fullscreen mode

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:

  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);
Enter fullscreen mode Exit fullscreen mode

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. 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:

aws cognito-idp admin-respond-to-auth-challenge 
Enter fullscreen mode Exit fullscreen mode

You can generate code via AWS-CLI, SDK, or by doing the HTTP request via POSTMAN or curl like this:

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>"
  }
}
'
Enter fullscreen mode Exit fullscreen mode

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:

{
  "sub": "sub",
  "cognito:groups": [
    "loan_creators"
  ],
  "iss": 'BLABLA",
  ...
  "auth_time": 1713899648,
  "exp": 1713903248,
  "iat": 1713899648,`
  "username": "daniel"
}
Enter fullscreen mode Exit fullscreen mode

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:

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}'
Enter fullscreen mode Exit fullscreen mode

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

The same for loan creation:

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}'
Enter fullscreen mode Exit fullscreen mode

If your user will be with a not allowed group you will obtain the below response:
Image description

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

Useful resources

Top comments (0)