How to build a C# ASP.NET Core web app & API that provides fine-grained access control

My GitHub repo use AAD App Roles & also provides fine-grained access control to data using Resource-based and Policy-based authorization.

Rules

In this example, we want to provide fine-grained access control using Azure Active Directory App Roles & Groups. This way, we don’t have to maintain the list of users and their roles in our application. Instead, we can keep our application focused on “role-based access”.

rules

In this example, there are 2 branches of the company & a corporate office. The following rules should apply:

  • Each person should be to read their own salary data
  • No person should be able to modify their own salary data
  • The regional manager of a branch should be able to read & write all salary data for their branch only
  • The CFO should be able to read & write all salary data for all employees (except themselves, of course)

Web App

As you sign in with different users, you will see that they have different permissions.

Salesperson

As someone with the General.ReadWrite role, Dwight can see his own salary, but not his fellow employees. He also cannot create a new salary record or modify his salary.

salesperson

Regional manager – Scranton

As someone with the RegionalManager.ReadWrite role, Michael can see his own salary and the salaries of his branch employees (but not those of the Stamford branch). He can modify his own employees salaries, but not his own.

salesperson

Regional manager – Stamford

As someone with the RegionalManager.ReadWrite role, Josh can see his own salary and the salaries of his branch employees (but not those of the Scranton branch). He can modify his own employees salaries, but not his own.

salesperson

CFO

As someone with the CFO.ReadWrite role, David can see his own salary and the salaries of all his employees. He can modify his employees salaries, but not his own.

salesperson

How this works

ID token for each user signin

We don’t want to have to constantly issue Graph API queries to find out information about the user, so we can request that every JWT ID token that is presented to the application include the AAD groups they are a part of (that are related to the application), the app roles the user is assigned (by virtue of being in those AAD groups) and the upn of the user (so we can key off it in the database).

jwtToken

Setup authorization service

In the src/DunderMifflinInfinity.API/Startup.cs file in the ConfigureServices method, we need to set up the authorization policies we want enforced.

startupConfigureServices
  • You can group several roles together into a single policy so that you don’t have to specify them multiple times.
  • You can define a list of requirements that must all return Succeeded for the policy to pass.

Add a new SalaryAuthorizationHandler to the list of services.

Authorization Service

In the src/DunderMifflinInfinity.API/AuthorizationHandlers/SalaryAuthorizationHandler.cs file, the SalaryAuthorizationHandler service is responsible for evaluating the list of requirements defined in the Salary policy.

It will loop through each requirement as defined in the Startup.cs file. For each requirement, a function will get called to evaluate it.

salaryAuthorizationHandlerLoop

For the CannotModifyOwnSalaryRequirement, we need to check the User.Identity.Name to see if it is the same UserPrincipalName of the Salary object the user is trying to modify. No one should be able to modify their own salary.

salaryAuthorizationHandlerCannotModifyOwnSalary

For the OnlyManagementCanModifySalariesRequirement, we need to check the roles the signed-in user has to see if they are in a management role.

salaryAuthorizationHandlerBranchManagerCanOnlyModifyOwnBranch

For the BranchManagerCanOnlyModifyOwnBranchSalariesRequirement, we need to check if the user is a regional manager, and if so, ensure they are only modifying data for their own branch employees.

salaryAuthorizationHandlerOnlyManagementCanModifySalaries

Note: In this case, the same policies are applied to both the web API & the web app. This means you could abstract out the AuthorizationHandlers into a separate library that both projects depend on. However, there are likely to be differences in a real-world application, so they are copied in this case so you have the ability to customize as needed.

API Controllers

Get

In the src/DunderMifflinInfinity.API/Controllers/SalariesController.cs file, we use two attributes. For the entire controller, we use the RequiredScope to ensure only users have the Salary.ReadWrite scope in their token before continuing. Then in the GetSalaries method, we use the Policies.General policy because everyone can see some salary data, but it will change depending on their role. We use Entity Framework to only pull the appropriate data for each role.

getSalaries

Put

In the src/DunderMifflinInfinity.API/Controllers/SalariesController.cs file, in the PutSalary method, we use the _authorizationService to evaluate if the signed-in user is allowed to modify the Salary object. If so, we make the database change, otherwise, we forbid it. This will call the SalaryAuthorizationService and loop through all requirements.

editSalary

Web App Controllers

The Web App controllers delegate accessing the web API to some helper Services (src/DunderMifflinInfinity.WebApp/Services) that get an access token when needed.

private async Task PrepareAuthenticatedClient()
{
  var accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(new[] { scope });
  httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
  httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}

private async Task<HttpResponseMessage> ExecuteApi(HttpMethod httpMethod, string apiEndpoint, StringContent? jsonContent = null)
{
  await PrepareAuthenticatedClient();
  HttpRequestMessage httpRequestMessage = new HttpRequestMessage(httpMethod, apiBaseAddress + apiEndpoint){
    Content = jsonContent
  };

  return await httpClient.SendAsync(httpRequestMessage);
}

When the application runs and tries to access the backend API for Salary data, an access token is procured for the Salary.ReadWrite scope.

requiredScope

The access token contains all the information we need to decide if the user should be allowed to modify the data. Notice that we have the aud (audience, the backend API app ID), the groups the user is a part of (that are related to this app), the app roles the user is a part of, the Salary.ReadWrite scope & the upn (user principal name) uniquely identifying the user.

accessToken

These services are registered in the src/DunderMifflinInfinity.WebApp/Startup.cs file.

services.AddHttpClient<IBranchApiService, BranchApiService>();
services.AddHttpClient<IEmployeeApiService, EmployeeApiService>();
services.AddHttpClient<ISalaryApiService, SalaryApiService>();
services.AddHttpClient<ISaleApiService, SaleApiService>();

Views

In the src/DunderMifflinInfinity.WebApp/Views/Salaries/Index.cshtml you can see that we hide the Create new button if the user is not a manager.

We also hide the Edit and Delete buttons if the user is not a manager & is not trying to modify their own salary.

view

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *