PEP and PDP for Secure Authorization with AVP

2025-02-20
This post cover image
Voice provided by Amazon Polly

In the first part, PEP and PDP for Secure Authorization with Cognito, I introduced the concept of Policy Enforcement Points (PEPs) and Policy Decision Points (PDPs) using Lambda Authorizer in API Gateway as PEP and a Lambda based service as PDP. The authorization decision was based on the roles users held, represented by Cognito groups, with the permissions mapped in a DynamoDB table.

When running a PEP and PDP setup in a multi-tenant SaaS solution we for sure need something more powerful than a DynamoDB table with permissions. Therefor in this part we will look at using Amazon Verified Permissions (AVP) as the foundation in our PDP. Which would serve as an great choice to do Authorization in SaaS.

What is Amazon Verified Permissions?

Amazon Verified Permissions (AVP) is a fully managed service that simplifies the implementation of fine-grained access control in our applications. AVP acts as a central Policy Decision Point (PDP), where it evaluates policies and makes authorization decisions based on the attributes of the request, user roles, and similar. It doesn't only support role-based access control (RBAC), but AVP also supports attribute-based access control (ABAC) and other dynamic policies, allowing for more flexible and granular authorization decisions.

With AVP, our Lambda function (acting as the PDP) no longer has to manually parse and evaluate policies, and we don’t need to store permission mapping in a DynamoDB table. Instead, we define and manage our policies directly in AVP, which then makes the authorization decision for us.

Alternatives Open Source solution to AVP

While AVP is an excellent choice for applications specially if they run in AWS, it’s not the only solution for implementing fine-grained authorization. One alternative is Oso, an open-source authorization framework designed to provide flexible, policy-based access control. Oso allows us to define authorization logic in a declarative language and integrates with our applications across various environments. Choosing between AVP and Oso depends on our specific use case, infrastructure etc.

Caching authorization decisions

Now that we have looked at using AVP as a central part of our PDP, we should think about caching. There are pros and cons against caching, for example AWS IAM never cache any decision which ensure that updates to policies are reflected immediately. With a cache we can reduce the number of calls to AVP and by that reduce cost. We can also either cache decisions in the client or in our backend systems. I will not go in to deep in this topic more than that in this solution we will use an external cache in our backend system. The cache will be placed in DynamoDB. The reason to cache in an external source, like DynamoDB, instead of in Lambda function memory, is for all invocations of the Lambda function to benefit from the cache. With a cache like this we would need a way to clear the cache if a permission set for a user is changed. I will not implement any cache invalidation mechanism in this post.

Understanding AVP and the Cedar Policy Language

Amazon Verified Permissions (AVP) uses Cedar, a purpose-built, policy-as-code language designed for fine-grained authorization. Cedar enables us to define and enforce access control policies that dictate who can perform what actions on which resources.

Cedar policies are declarative, meaning they explicitly state the permissions without requiring procedural logic. They are designed to be easy to read while supporting powerful conditions and attribute-based access control (ABAC).

Basic Cedar Policy Example

Let's start with a simple RBAC (Role-Based Access Control) policy that allows users in the "Admin" group to perform certain actions on all resources

permit (
principal in UserGroup::"Admin",
action in [
Action::"create",
Action::"read",
Action::"update",
Action::"delete"
],
resource
);

This policy means that any principal (user) who belongs to the UserGroup Admin is permitted to perform the listed actions on all resources, since resource is not restricted.

A second example, where we lean towards a ABAC method would be.

permit (
principal in UserGroup::"JimmysFriends",
action == Action::"viewPhoto",
resource
)
when {
resource.tags.contains("Shared")
};

This means that users in JimmysFriends can view photos when the photo has the Shared tag.

In a multi-tenant SaaS system, we might want to restrict access to data belonging to a specific tenant. Cedar allows attribute-based conditions for this.

permit (
principal,
action == Action::"view",
resource
)
when {
principal.tenant == resource.tenant
};

This ensures that the user can only view resources that belong to the same tenant.

In the implementation for our AVP based PDP we will use RBAC and policy similar to the first example.

Using AVP for scalable and flexible access control

By integrating (AVP) as part of our central PDP, we have simplified our policy management and enhanced the scalability and flexibility of the authorization system. AVP’s support for dynamic policies, and fine-grained access control are powerful tools for any SaaS application.

Implementation

With the introduction to AVP let's convert our PDP to use AVP for Role Based Access (RBAC) instead of a DynamoDB permission mapping. We will just deploy a second PDP into our solution and the swap so our PEP use the new PDP based on AVP instead. This entire setup is based on the solution introduced in part one, PEP and PDP for Secure Authorization with Cognito,, and as prerequisite that solution should be deployed. You find the entire solution on Serverless Handbook PEP and PDP.

Architecture Overview

Just as a reminder, the entire code and all of the architecture can be found on Serverless Handbook PEP and PDP

Looking at the overview of the architecture and call flow we can see that there is not any major changes, but instead of fetching a permission mapping from DynamoDB we will call AVP and let the service use it's policy engine to allow/deny decision.

Image showing the architecture overview

To better understand the flow during an API access.

Image showing the call flow

Setup and deploy AVP based PDP

First we need to deploy all the resources needed by AVP, what we need to do is to create a Policy Store with our policy schema. The Policies for our three different Roles with a Cedar based policy to determine what the Role has permission to do, and we need to setup our Identity Source.

This template can be fairly long as the Cedar polices and schema tend to be rather huge. So parts of this template has been left out, therefor visit Serverless Handbook PEP and PDP for the full template.

AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: PDP Service
Parameters:
ApplicationName:
Type: String
Description: Name of owning application
UserManagementStackName:
Type: String
Description: The name of the stack that contains the user management part, e.g the Cognito UserPool
AVPNameSpace:
Type: String
Description: The name space for Amazon Verified Permissions
AllowedPattern: "[a-z]+"
UserPoolId:
Type: String
Description: The ID of the Cognito User Pool

Resources:
PolicyStore:
Type: AWS::VerifiedPermissions::PolicyStore
Properties:
Description: !Sub Policy Store for ${ApplicationName}
ValidationSettings:
Mode: "OFF"
Schema:
CedarJson: !Sub |
{
"${AVPNameSpace}": {
"entityTypes": {
"CognitoUser": {
"shape": {
"type": "Record",
"attributes": {}
},
"memberOfTypes": [
"CognitoUserGroup"
]
},
"CognitoUserGroup": {
"shape": {
"attributes": {},
"type": "Record"
}
},
"Application": {
"shape": {
"attributes": {},
"type": "Record"
}
}
},
"actions": {
"get /rider": {
"appliesTo": {
"context": {
"type": "Record",
"attributes": {}
},
"principalTypes": [
"CognitoUser"
],
"resourceTypes": [
"Application"
]
}
},
"get /riders": {
"appliesTo": {
"context": {
"type": "Record",
"attributes": {}
},
"principalTypes": [
"CognitoUser"
],
"resourceTypes": [
"Application"
]
}
},
"get /trainer": {
"appliesTo": {
"context": {
"type": "Record",
"attributes": {}
},
"principalTypes": [
"CognitoUser"
],
"resourceTypes": [
"Application"
]
}
},
"get /trainers": {
"appliesTo": {
"context": {
"type": "Record",
"attributes": {}
},
"principalTypes": [
"CognitoUser"
],
"resourceTypes": [
"Application"
]
}
},
"get /unicorn": {
"appliesTo": {
"context": {
"type": "Record",
"attributes": {}
},
"principalTypes": [
"CognitoUser"
],
"resourceTypes": [
"Application"
]
}
},
"get /unicorns": {
"appliesTo": {
"context": {
"type": "Record",
"attributes": {}
},
"principalTypes": [
"CognitoUser"
],
"resourceTypes": [
"Application"
]
}
}
}
}
}


CognitoIdentitySource:
Type: AWS::VerifiedPermissions::IdentitySource
Properties:
Configuration:
CognitoUserPoolConfiguration:
ClientIds:
- Fn::ImportValue: !Sub ${UserManagementStackName}:app-audience
GroupConfiguration:
GroupEntityType: !Sub ${AVPNameSpace}::CognitoUserGroup
UserPoolArn:
Fn::ImportValue: !Sub ${UserManagementStackName}:user-pool-arn
PolicyStoreId: !Ref PolicyStore
PrincipalEntityType: !Sub ${AVPNameSpace}::CognitoUser

AdminUserPolicy:
Type: AWS::VerifiedPermissions::Policy
Properties:
Definition:
Static:
Description: Policy for Admin User group in Cognito
Statement: !Sub |
permit(
principal in ${AVPNameSpace}::CognitoUserGroup::"${UserPoolId}|Admin",
action in [ ${AVPNameSpace}::Action::"get /rider", ${AVPNameSpace}::Action::"get /riders", ${AVPNameSpace}::Action::"get /trainer", ${AVPNameSpace}::Action::"get /trainers", ${AVPNameSpace}::Action::"get /unicorn", ${AVPNameSpace}::Action::"get /unicorns" ],
resource
);

PolicyStoreId: !Ref PolicyStore

LambdaPDPFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: Lambda/AuthZ
Handler: authz.handler
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref AVPCacheTable
- Version: "2012-10-17"
Statement:
Effect: Allow
Action:
- "verifiedpermissions:EvaluatePolicy"
- "verifiedpermissions:GetPolicy"
- "verifiedpermissions:IsAuthorizedWithToken"
Resource: "*"
Environment:
Variables:
JWKS_URL:
Fn::ImportValue: !Sub ${UserManagementStackName}:jwks-url
AUDIENCE:
Fn::ImportValue: !Sub ${UserManagementStackName}:app-audience
POLICY_STORE_ID:
!Ref PolicyStore
NAMESPACE:
!Ref AVPNameSpace
TOKEN_TYPE: "accessToken"

Authorization logic

The PDP Lambda will get the jwt-token, action, and resource from the PEP, our Lambda Authorizer in the API Gateway. The function will get the principal, the subject, from the JWT-Token and see if there is a cached decision. If there is not it will call AVP with all information to get an authorization decision.

import json
import os
import base64
import boto3
import time
from datetime import datetime, timedelta, timezone

policy_store_id = os.getenv("POLICY_STORE_ID")
namespace = os.getenv("NAMESPACE")
token_type = os.getenv("TOKEN_TYPE")
resource_type = f"{namespace}::Application"
resource_id = namespace
action_type = f"{namespace}::Action"
table_name = os.environ["PERMISSION_CACHE_TABLE"]

avp_client = boto3.client("verifiedpermissions")
dynamodb_client = boto3.client("dynamodb")


def decode_token(bearer_token):
return json.loads(base64.b64decode(bearer_token.split(".")[1]).decode("utf-8"))


def generate_access(principal, effect, action, resource):
auth_response = {
"statusCode": 200 if effect == "Allow" else 403,
"principalId": principal,
"effect": effect,
"action": action,
"resource": resource,
}
return auth_response


def get_auth_cache(principal, action):
try:
response = dynamodb_client.get_item(
TableName=table_name, Key={"PK": {"S": principal}, "SK": {"S": action}}
)

item = response.get("Item")
if not item:
return None

ttl = int(item.get("TTL")["N"])
if ttl and ttl < int(time.time() * 1000): # TTL is in milliseconds
return None

return item.get("Effect")["S"]

except Exception as e:
print(f"Error getting auth cache: {e}")
return None


def store_auth_cache(principal, action, auth_response):
try:
ttl = int((datetime.now(timezone.utc) + timedelta(hours=12)).timestamp() * 1000)

effect = (
"Allow" if auth_response.get("decision", "").upper() == "ALLOW" else "Deny"
)

dynamodb_client.put_item(
TableName=table_name,
Item={
"PK": {"S": principal},
"SK": {"S": action},
"TTL": {"N": str(ttl)},
"Effect": {"S": effect},
},
)

except Exception as e:
print(f"Error storing auth cache: {e}")


def validate_permission(event):
jwt_token = event["jwt_token"]
resource = event["resource"]
action = event["action"]
parsed_token = decode_token(jwt_token)

try:
action_id = f"{action.lower()} {resource.lower()}"
user_principal = parsed_token["sub"]

cached_auth = get_auth_cache(user_principal, action_id)

if cached_auth is None:
auth_response = avp_client.is_authorized_with_token(
accessToken=jwt_token,
policyStoreId=policy_store_id,
action={"actionType": action_type, "actionId": action_id},
resource={"entityType": resource_type, "entityId": resource_id},
)

store_auth_cache(user_principal, action_id, auth_response)

response_body = generate_access(
user_principal,
"Allow" if auth_response["decision"].upper() == "ALLOW" else "Deny",
action,
resource,
)

return {
"statusCode": 200,
"body": json.dumps(response_body),
"headers": {"Content-Type": "application/json"},
}

else:
response_body = generate_access(
user_principal, cached_auth, action, resource
)

return {
"statusCode": 200,
"body": json.dumps(response_body),
"headers": {"Content-Type": "application/json"},
}

except Exception as e:
print(f"Error validating permissions: {e}")

response_body = generate_access(parsed_token["sub"], "Deny", action, resource)

return {
"statusCode": 200,
"body": json.dumps(response_body),
"headers": {"Content-Type": "application/json"},
}


def handler(event, context):
print(f"Event: {json.dumps(event)}")
permissions = validate_permission(event)
return permissions

Update PEP to use AVP Based PDP

To update our PEP to use the new AVP based PDP instead of the DynamoDB based, navigate to the API folder and modify the samconfig.yaml file, update the value of PDP to PDPStackName=pep-pdp-cognito-pdp-auth-service-avp then redeploy the API part of the solution. This will now swap the PDP that is used.

Testing

To test the setup of AVP we can navigate to the AVP part of the console.

Under Policy Stores you should see the policy store that was created for our PDP.

Image showing the policy store list

By clicking on the ID of the policy store and selecting Policies in the menu we see the list of the three created policies.

Image showing the policy list

By selecting the Riders policy we can now inspect the create policy.

Image showing the policy list

By selecting Schema in the menu we can inspect the created schema in a visual form.

Image showing the policy schema

Image showing the policy schema

Now, we can navigate to the Test Bench to test out our policies, fill in the information as shown in the image below. The group must be prefixed with the Cognito User Pool Id and follow pattern <COGNITO_USER_POOL_ID>|<GROUP_NAME>

Image showing the test bench

If we select the action /get trainers and click Run Authorization request we should get a deny back, as the Trainer role don't have access to that Action

Image showing the test bench

Swapping to the /get trainer action should instead give us an allow back.

Image showing the test bench

Summary and conclusion

Implementing PEP and PDP in our authorization flow offers a highly scalable, flexible, and secure way to control access to resources. By leveraging AWS Lambda and API Gateway, we can build a serverless authorization system that separates authentication and authorization concerns, scales with demand, and simplifies policy management.

With the addition of Role-Based Access Control and Amazon Verified Permissions (AVP), combined with caching for enhanced performance, we can create an authorization solution that fits both current and future needs. Using AVP in our SaaS solutions give us a very powerful way to handle multi tenancy.

Happy coding, and stay secure!

Source Code

The entire setup, with detailed deployment instructions, and all the code can be found on Serverless Handbook PEP and PDP

Final Words

Don't forget to follow me on LinkedIn and X for more content, and read rest of my Blogs

As Werner says! Now Go Build!


Post Quiz

Test what you just learned by doing this five question quiz - https://kvist.ai/581622.
Scan the QR code below or click the link above.

Powered by kvist.ai your AI generated quiz solution!