.NET on AWS Blog

Implement a Custom Authorization Policy Provider for ASP.NET Core Apps using Amazon Verified Permissions

Amazon Verified Permissions is a managed authorization service for custom applications. You can use Verified Permissions to define fine-grained authorization policies based on principals, resources, roles, and attributes. Verified Permissions enables developers to build secure applications faster by externalizing authorization and centralizing policy management and administration. In this blog post, I use Verified Permissions to implement a custom authorization policy provider for an ASP.NET Core application.

Introduction to the TinyTodo application

The sample application used in this post allows users to create, share, and delete to-do lists, and uses a SQLite database to store entities. If you want to deploy the app and follow along, deploy the TinyTodo.CDK project. For instructions about deploying AWS Cloud Development Kit (CDK) projects, see Building, synthesizing, and deploying in the AWS CDK Development Kit Developer Guide.

The sample application enforces the following authorization rules in this application:

  • Allow users to create, add items to, share, and delete to-do lists they own.
  • Allow users to add items to a shared to-do list.
  • Allow users to re-share a shared to-do list if permitted by the user who shared the to-do list.
  • Allow only administrators to access the admin module.

Users can share to-do lists with other users. While sharing a to-do list, they can control if the other user can re-share the to-do list with other users.

share to-do list dialog

Users can add items to the to-do lists shared with them.

new to-do item dialog

Users should see an error message when they try to re-share a to-do list which wasn’t shared with the required permissions.

error message: you don't have permissions to share the to-do list

Users who are not administrators should see an error page when they try to access the admin module.

access denied error page

Implementation

Set up authorization policies and create a policy store

Verified Permissions uses the Cedar policy language to define fine-grained authorization policies. A schema is a declaration of the structure of the entity types and actions that you want to support in your application. The sample application uses the schema in file TinyTodoList.cedarschema.json to define entity types and actions.

A policy is a statement that either permits or forbids a principal to take one or more actions on a resource.

In Cedar, you can define authorization rules in two ways. The first option is to have small set of policies that apply to a large group of principal and resources. I will implement this using Static Policies. Static policies allow or deny principals to perform specified actions on specified resources for your application. For example, the rule that allows access to the admin module is implemented using a static policy. Following is an example of the policy.

permit (
  principal,
  action in [TinyTodoList::Action::"UserAdmin"],
  resource
) when {
  principal.Role == "Administrator"
};

The second option is to have a policy per principal or resource or both. Every time access is granted, the application creates a policy in Verified Permissions. I will implement this using policy templates. A policy template is a policy with placeholders for the principal and resource. These policy templates are instantiated by the sample application at run time to create new template-linked policies. For example, when a user shares a to-do list with another user, the following policy template is used.

permit (
  principal in ?principal,
  action in [TinyTodoList::Action::"AddTodoItem"], 
  resource in ?resource
);

Following is a template-linked policy created by the application, based on the preceding policy template when user1@example.com shares a to-do list with user2@example.com, with permissions to add items only.

permit (
  principal in TinyTodoList::User::"user2@example.com",
  action in [TinyTodoList::Action::"AddTodoItem"], 
  resource in TinyTodoList::TodoList::"c79c922b-7164-404b-a3e5-3bc023ff5525"
);

To create this policy store with policies, deploy the TinyTodo.CDK project as described earlier.

Make a note of your PolicyStoreId, the TodoListSharedAccessPolicyTemplateId, and the TodoListSharedAccessWithResharePolicyTemplateId. You will need these values in the next step.

CDK outputs

Integrate with ASP.NET Core

Add policy store configuration to the application

In the sample application, every time a user takes an action, the application will call Verified Permissions to check whether the action is authorized. Therefore you need to link the application with Verified Permissions. I link the application with Verified Permissions by adding the following configurations to the appsettings.json file and replace the placeholder values for PolicyStoreId and template ids copied from the previous step.

"VerifiedPermissions": {
    "PolicyStoreId": "XXXXXXXXXXXXX",
    "PolicyStoreSchemaNamespace": "TinyTodoList",
    "TodoListSharedAccessPolicyTemplateId": "XXXXXXXXXXXXX",
    "TodoListSharedAccessWithResharePolicyTemplateId": "XXXXXXXXXXXXX"    
  }

Authorize access to controller actions

The sample application authorizes controller actions by adding the HasPermissionOnAction custom authorize attribute.
The following listed code authorizes actions on the admin module (AdminController.cs).

[HasPermissionOnAction(Constants.Actions.UserAdmin)]
public IActionResult UserAdmin()
{
    return View();
}

The following code authorizes actions on the to-do list. In addition to passing the action, this code also provides the resource type and name of the form element with resource id value, which is then used by the authorization handler to build a resource.

[HasPermissionOnAction(actionName: Constants.Actions.ShareTodoList, 
                resourceType: nameof(TodoList), 
                resourceIdFormElementName: nameof(TodoListShare.TodoListId))]
public async Task<IActionResult> ShareAsync(TodoListShare todoListShare)
{
    ...
}

Create a custom authorization policy provider

An ASP.NET Core authorization policy is a collection of authorization requirements. An authorization requirement is a collection of parameters that an authorization handler can use to evaluate the current user principal for access on a resource or controller action.

For this blog post, we are using the action name, resource type, and the resource id element name as the parameters on authorization requirements. These parameters are used by the authorization handlers can build an authorization request for Verified Permissions by using the Principal, Action, and Resource details.

First, create an authorization requirement for authorizing controller actions and resources (HasPermissionRequirement.cs).

public class HasPermissionRequirement : IAuthorizationRequirement
{
    public HasPermissionRequirement(string action, string resourceType, string resourceIdFormElementName)
    {
        Action = action;
        ResourceType = resourceType;
        ResourceIdFormElementName = resourceIdFormElementName;
    }
}

Now create a custom policy provider to build and return a policy with requirements based on a policy name (CustomAuthorizationPolicyProvider.cs).

public async Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
    var policyRequirements = await GetPolicyRequirements(policyName);
    var policyBuilder = new AuthorizationPolicyBuilder();   
    policyBuilder.AddRequirements(policyRequirements);
    return policyBuilder.Build();
}

private async Task<IAuthorizationRequirement[]> GetPolicyRequirements(string policyName)
{
    if(policyName.StartsWith(Constants.PolicyPrefixes.HasPermissionOnAction))
   {
      var action = policyName.Replace(Constants.PolicyPrefixes.HasPermissionOnAction, "");
      var actionParts = action.Split("_");
      return new [] {new HasPermissionRequirement(action: actionParts[0], resourceType: actionParts[1], 
                                                    resourceIdFormElementName: actionParts[2])};
   }    

    throw new NotImplementedException("Unknown policy type");
}

Authorization handlers are responsible for evaluating the authorization requirements against the user principal and the resource object available in the authorization context. For the example in this post, authorization handlers make use of the Verified Permissions API to obtain authorization decisions (HasPermissionRequirementHandler.cs).

  • Make a Verified Permissions API call and validate the authorization result (VerifiedPermissionsUtil.cs).
var principal = ToEntityItem(user);

var authorizationRequest = new IsAuthorizedRequest
{
   PolicyStoreId = _appConfig.PolicyStoreId,
   Principal = principal.Identifier,
   Action = new ActionIdentifier { ActionType = _appConfig.ActionType, ActionId = action },
   Entities = new EntitiesDefinition
   {
      EntityList = new List<EntityItem> { principal }
   }
};

if (entity != null)
{
   var resource = ToEntityItem(entity);
   authorizationRequest.Resource = resource.Identifier;
   authorizationRequest.Entities.EntityList.Add(resource);
}

var isAuthorizedResponse = await _verifiedPermissionsClient.IsAuthorizedAsync(authorizationRequest);var isAuthorizedResponse = await _verifiedPermissionsUtil.IsAuthorizedAsync(context.User, requirement.Action, resource);

if (isAuthorizedResponse.Decision == Decision.ALLOW)
{
   context.Succeed(requirement);
}
else
{
   context.Fail();
}
  • The sample application build a Verified Permissions authorization request using Principal, Action, and Resource (VerifiedPermissionsUtil.cs).
    • Principal: Build the Principal entity based on the claims from the ASP.NET Core ClaimsPrincipal. You need to map the identity claims to the principal attributes defined in the Verified Permissions schema. If you use Cognito you can pass the identity/access JWT tokens in the IsAuthorizedWithTokenRequest request.
public EntityItem ToEntityItem(ClaimsPrincipal? user)
{
   return new EntityItem
   {
      Identifier = new EntityIdentifier
      {
          EntityType = $"{_appConfig.PolicyStoreSchemaNamespace}::User",
          EntityId = user?.Identity?.Name
      },
      Attributes = new Dictionary<string, AttributeValue>
      {
          {"Email", new AttributeValue {String = user.FindFirstValue(ClaimTypes.Email)}},
          {"Role", new AttributeValue {String = user.FindFirstValue(ClaimTypes.Role)}}
      },
   };
}
    • Action: Use the action name passed into the authorization requirement.

new ActionIdentifier { ActionType = _appConfig.ActionType, ActionId = action }

    • Resource: Build a Resource entity based on the attributes of the Resource object passed into the authorization handler.
public EntityItem ToEntityItem(IEntity resource)
{
   if (resource == null)
   {
      return null;
   }

   var entityItem = new EntityItem
   {
      Identifier = new EntityIdentifier
      {
         EntityType = $"{_appConfig.PolicyStoreSchemaNamespace}::{resource.GetType().Name}",
         EntityId = $"{resource.Id}"
      },
      Attributes = ToDictionary(resource)
   };
   return entityItem;
}

Finally, register the custom authorization policy provider and the authorization handlers with ASP.NET Core service container (Program.cs).

builder.Services.AddSingleton<IAuthorizationPolicyProvider, CustomAuthorizationPolicyProvider>();
builder.Services.AddTransient<IAuthorizationHandler, HasPermissionRequirementHandler>();

Implementing to-do list sharing

When a user shares a to-do list with another user, a template-linked policy is created as shown in the following listing (VerifiedPermissionsUtil.cs). The code shows creating policies in Verified Permissions when a user shares a to-do list.

var policyDefinition = new PolicyDefinition
{
    TemplateLinked = new TemplateLinkedPolicyDefinition
    {
        PolicyTemplateId = policyTemplateId,
        Principal = principal,
        Resource = resource
    }
};

await _verifiedPermissionsClient.CreatePolicyAsync(new CreatePolicyRequest
{
    PolicyStoreId = _appConfig.PolicyStoreId,
    Definition = policyDefinition
});

Multi Tenancy

To support multi-tenant applications, you could define separate policy stores for each tenant (siloed approach). The application needs to identify the tenant’s PolicyStoreId based on the request context and use that PolicyStoreId in the authorization requests to Verified Permissions.

Clean up

To clean up the resources created by the TinyTodo.CDK project, Log in to your AWS Management console and open the AWS CloudFormation console. Select TinyTodoCdkStack and delete the stack or run the following command.

cdk destroy TinyTodoStack

Conclusion

This blog post showed how to use Amazon Verified Permissions to implement a custom authorization policy provider for an ASP.NET Core application. Externalizing authorization by using Verified Permissions helps you define and manage fine-grained authorization policies for custom applications without changing the code frequently.

To learn more about Verified Permissions, see the Amazon Verified Permissions User Guide. For more information about the Cedar policy language grammar, see Grammar specification for Cedar policy syntax.