Introduction
In the previous session, we learned how to implement basic token authentication using access tokens and refresh tokens. In this session, we will focus on another crucial aspect of securing a web API: authorization.
Building on our existing application, we will implement the three main authorization techniques: role-based authorization, claims-based authorization and policy-based authorization.
This guide will contain the following sections:
- Initial configuration
- Role-based authorization
- Claims-based authorization
- Policy-based authorization
Initial configuration
For this example, we will extend the WeatherForecastController
that comes with a new .NET Core web API project. Before implementing each authorization method, please complete the following steps, as we will need them later:
Step 1 - Add a new constant inside the ApplicationClaims
class
namespace DemoBackend.Constants
{
public static class ApplicationClaims
{
//defined in the previous session
public const string Id = "userId";
public const string Email = "userEmail";
public const string Age = "userAge";
public const string Role = "userRole";
//we are going to use this to demonstrate claims-based authorization
public const string AccessToPremium = "accessToPremium";
}
}
Step 2 - Add a new static PoliciesConstants
class
namespace DemoBackend.Constants
{
public static class UserTokenConstants
{
public const string Refresh = "refresh";
public const string LocalProvider = "local";
}
}
Step 3 - Register more users for testing
In the previous session, we registered one user with the role USER
and age = 25
. To properly test authorization, we need to register two more users:
- Another user with role
USER
andage = 16
. Here is the curl for this:
curl --location 'http://localhost:8080/api/usermanagement/register' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "testuser1@example.com",
"password": "Test@1234",
"confirmPassword": "Test@1234",
"role": "USER",
"age": 16
}'
- One user with role
ADMIN
andage = 30
. Here is the curl for this:
curl --location 'http://localhost:8080/api/usermanagement/register' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "testadmin@example.com",
"password": "Test@1234",
"confirmPassword": "Test@1234",
"role": "ADMIN",
"age": 30
}'
The AspNetUsers
table should look like this:
Step 4 - Create WeatherForecastController
if you do not have it yet
Assure that you route
corresponds with the one bellow:
[ApiController]
[Route("api/[controller]")]
public class WeatherForecastController : ControllerBase
{
//authorization code will go here
}
Now that we have the base setup, we can proceed to exemplify the 3 types of authorization.
Role-based authorization
Step 1 - Setup
The prerequisite for this authorization method is to have System.Security.Principal.ClaimsType.Role
added to the access token.
Inside the TokenProviderHelper.GenerateAccessToken
method, add the following line:
// method implementation
var claimsForToken = new List<Claim>
{
//other claims
new(ClaimTypes.Role, role.ToUpper()),
//other claims
};
// method implementation
Step 2 - Controller methods
Let's add the following GET
methods: GetUserWeather
and GetAdminWeather
. Notice that they are marked with [Authorize(Roles = ApplicationRoles.User)]
and [Authorize(Roles = ApplicationRoles.Admin)]
, respectively.
This means that:
- The first method can only be accessed by a logged-in user with the
USER
role - The second method can only be accessed by a logged-in user with the
ADMIN
role
namespace DemoBackend.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class WeatherForecastController : ControllerBase
{
//roles authorization
[Authorize(Roles = ApplicationRoles.User)]
[HttpGet]
[Route("user-weather")]
public IActionResult GetUserWeather()
{
return Ok(Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = $"Only the {ApplicationRoles.User} has access to this"
}).ToArray());
}
[Authorize(Roles = ApplicationRoles.Admin)]
[HttpGet]
[Route("admin-weather")]
public IActionResult GetAdminWeather()
{
return Ok(Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = $"Only the {ApplicationRoles.Admin} has access to this"
})
.ToArray());
}
}
}
Step 3 - Testing
- Log in with a user that has the role
USER
, then callGetUserWeather
using the received access token—you should get the weather records. However, if you try to accessGetAdminWeather
, you should receive a403 Forbidden
status. Here is the curl command:
curl --location 'http://localhost:8080/api/weatherforecast/user-weather' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjZDEyNWVlZi1jMDUwLTQ3MDktYTZjZi0yNmYxNDRkMmMzNDUiLCJ1c2VyRW1haWwiOiJ0ZXN0dXNlckBleGFtcGxlLmNvbSIsInVzZXJBZ2UiOiIyNSIsInVzZXJSb2xlIjoiVVNFUiIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IlVTRVIiLCJqdGkiOiI5YWU2YTI2Yi1jYTQwLTQzMjctOTY1Ny00NjcwOGU5ZjIxYWEiLCJuYmYiOjE3MzgzMjUxODksImV4cCI6MTczODMyODc4OSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiYXVkIjoibXktd2ViLWFwaS1jbGllbnQifQ.mvwdpmgHCyHpbX5FbflUMaTw73Cry_sQQ8Hnv2XikRI'
- Vice-versa, log in with a user that has the role
ADMIN
, then callGetAdminWeather
using the received access token—you should get the weather records. However, if you try to accessGetUserWeather
, you should receive a403 Forbidden
status. Here is the curl command:
curl --location 'http://localhost:8080/api/weatherforecast/admin-weather' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIyZjk4ZjAwMC1kYWUwLTQzODMtODNjMy0yZDczMGI3MTRkYzQiLCJ1c2VyRW1haWwiOiJ0ZXN0YWRtaW5AZXhhbXBsZS5jb20iLCJ1c2VyQWdlIjoiMzAiLCJ1c2VyUm9sZSI6IkFETUlOIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQURNSU4iLCJqdGkiOiJjNjliZjQ5NS00ZGNhLTQ3NmYtYjVlYS02M2EzOWIzOTRhNzkiLCJuYmYiOjE3MzgzMjYyNTQsImV4cCI6MTczODMyOTg1NCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiYXVkIjoibXktd2ViLWFwaS1jbGllbnQifQ.z5f2m9Iy__TbP5bpoEoGqXM5es9q-ur1gmXH31VetFI'
Claims-based authorization
Step 1 - Setup
This type of authorization verifies whether an access token contains a specific claim and checks the value of that claim.
For example, let’s assume that a user under 18 is eligible for premium access to weather information. In code, we can enforce this by adding the following condition inside TokenProviderHelper.GenerateAccessToken
:
//if you are under 18 you get access to premium content
if (user.Age < 18)
{
claimsForToken.Add(new(ApplicationClaims.AccessToPremium, ApplicationClaims.AccessToPremium));
}
The second thing we need to do is to setup this check. We will create a new extension method for this:
namespace DemoBackend.Extensions
{
public static class AuthorizationExtentions
{
public static void AddAuthorizationPolicies(this IServiceCollection services)
{
services.AddAuthorization(options =>
{
//claims based authorization
options.AddPolicy(PoliciesConstants.Premium,
policy => policy.RequireClaim(ApplicationClaims.AccessToPremium, ApplicationClaims.AccessToPremium));
});
}
}
}
Don't forget to register this new extension method in Program.cs
:
//register the authorization policies
builder.Services.AddAuthorizationPolicies();
Step 2 - Controller methods
To apply the authorization we need to apply the following attribute: [Authorize(Policy = PoliciesConstants.Premium)]
.
namespace DemoBackend.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class WeatherForecastController : ControllerBase
{
//code omitted for brevity
//claims based authorization
[Authorize(Policy = PoliciesConstants.Premium)]
[HttpGet]
[Route("premium-weather")]
public IActionResult GetPremiumWeather()
{
return Ok(Enumerable.Range(1, 7).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = $"This is premium weather information"
})
.ToArray());
}
}
}
Step 3 - Testing
We need to log in with a user whose age is under 18. If we attempt to access this method with any other user, we will receive a 403 Forbidden status
. Here is the curl
command for this:
curl --location 'http://localhost:8080/api/weatherforecast/premium-weather' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJmYzAwYzkxMy0zYTk3LTRlYmQtYjQ2ZC1hNTQ0MjIwZjkwMjUiLCJ1c2VyRW1haWwiOiJ0ZXN0dXNlcjFAZXhhbXBsZS5jb20iLCJ1c2VyQWdlIjoiMTYiLCJ1c2VyUm9sZSI6IlVTRVIiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJVU0VSIiwianRpIjoiZGVhYzRjYmMtNGMwNS00NGM3LWI2ZGUtZWRhMGJiYTNmNTBiIiwiYWNjZXNzVG9QcmVtaXVtIjoiYWNjZXNzVG9QcmVtaXVtIiwibmJmIjoxNzM4MzI2MzA4LCJleHAiOjE3MzgzMjk5MDgsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCIsImF1ZCI6Im15LXdlYi1hcGktY2xpZW50In0.IluKD1N5jveY1xMYrejbODfBMTH7nvOCUkrAtFf57m0'
Policy-based authorization
Step 1 - Setup
Let’s say we want to restrict certain content for underage users (age < 18
). To achieve this, we can create a policy for this specific use case.
The first step is to define a requirement, which would look something like this:
namespace DemoBackend.Policies
{
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public int MinimumAge { get; }
public MinimumAgeRequirement(int minimumAge) => MinimumAge = minimumAge;
}
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumAgeRequirement requirement)
{
var age = context.User.FindFirst(ApplicationClaims.Age);
if (age is null)
{
return Task.CompletedTask;
}
if (int.TryParse(age.Value, out var userAge))
{
if (userAge >= requirement.MinimumAge)
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
}
The second step would be to register this requirement. We can do it in the same extension method defined above. Final version of this extension method is:
namespace DemoBackend.Extensions
{
public static class AuthorizationExtentions
{
public static void AddAuthorizationPolicies(this IServiceCollection services)
{
services.AddAuthorization(options =>
{
//claims based authorization (define above)
options.AddPolicy(PoliciesConstants.Premium,
policy => policy.RequireClaim(ApplicationClaims.AccessToPremium, ApplicationClaims.AccessToPremium));
//policy based authorization
options.AddPolicy(PoliciesConstants.AgeRestriction,
policy => policy.Requirements.Add(new MinimumAgeRequirement(18)));
});
//register the age requirement
services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
}
}
}
Step 2 - Controller methods
Now that the policy has been register, we can apply it to our method by adding the [Authorize(Policy = PoliciesConstants.AgeRestriction)]
:
namespace DemoBackend.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class WeatherForecastController : ControllerBase
{
//code omitted for brevity
//policy based authorization (must be at least 18 to access)
[Authorize(Policy = PoliciesConstants.AgeRestriction)]
[HttpGet]
[Route("age-restricted-weather")]
public IActionResult GetAgeRestrictedWeather()
{
return Ok(Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = $"This wheather info is age restricted"
})
.ToArray());
}
}
}
Step 3 - Testing
To properly test this, we need to log in with a user whose age is greater than 18
. If we attempt to access this method with an underage user, we should receive a 403 Forbidden status
. Here is the curl
command for this:
curl --location 'http://localhost:8080/api/weatherforecast/age-restricted-weather' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjZDEyNWVlZi1jMDUwLTQ3MDktYTZjZi0yNmYxNDRkMmMzNDUiLCJ1c2VyRW1haWwiOiJ0ZXN0dXNlckBleGFtcGxlLmNvbSIsInVzZXJBZ2UiOiIyNSIsInVzZXJSb2xlIjoiVVNFUiIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IlVTRVIiLCJqdGkiOiI5YWU2YTI2Yi1jYTQwLTQzMjctOTY1Ny00NjcwOGU5ZjIxYWEiLCJuYmYiOjE3MzgzMjUxODksImV4cCI6MTczODMyODc4OSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiYXVkIjoibXktd2ViLWFwaS1jbGllbnQifQ.mvwdpmgHCyHpbX5FbflUMaTw73Cry_sQQ8Hnv2XikRI'
Conclusion
In this session, we implemented the three types of authorization:
- Role-based authorization
- Claims-based authorization
- Policy-based authorization
Personally, I prefer using claims-based and policy-based authorization because they offer greater flexibility. Role-based authorization can often be handled using one of the other two methods, making it less essential in many cases.
In the next and final session, we will cover email and password changes using Identity, as well as discuss additional security options for a .NET Core API.
Top comments (0)