DEV Community

Cover image for Authorization using .NET Core Identity
Marian Salvan
Marian Salvan

Posted on

Authorization using .NET Core Identity

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:

  1. Initial configuration
  2. Role-based authorization
  3. Claims-based authorization
  4. 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";
    }
}
Enter fullscreen mode Exit fullscreen mode

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

Step 3 - Register more users for testing

In the previous session, we registered one user with the role USERand age = 25. To properly test authorization, we need to register two more users:

  • Another user with role USER and age = 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
}'
Enter fullscreen mode Exit fullscreen mode
  • One user with role ADMIN and age = 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
}'
Enter fullscreen mode Exit fullscreen mode

The AspNetUsers table should look like this:

Image description

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

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

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

Step 3 - Testing

  • Log in with a user that has the role USER, then call GetUserWeather using the received access token—you should get the weather records. However, if you try to access GetAdminWeather, you should receive a 403 Forbidden status. Here is the curl command:
curl --location 'http://localhost:8080/api/weatherforecast/user-weather' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjZDEyNWVlZi1jMDUwLTQ3MDktYTZjZi0yNmYxNDRkMmMzNDUiLCJ1c2VyRW1haWwiOiJ0ZXN0dXNlckBleGFtcGxlLmNvbSIsInVzZXJBZ2UiOiIyNSIsInVzZXJSb2xlIjoiVVNFUiIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IlVTRVIiLCJqdGkiOiI5YWU2YTI2Yi1jYTQwLTQzMjctOTY1Ny00NjcwOGU5ZjIxYWEiLCJuYmYiOjE3MzgzMjUxODksImV4cCI6MTczODMyODc4OSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiYXVkIjoibXktd2ViLWFwaS1jbGllbnQifQ.mvwdpmgHCyHpbX5FbflUMaTw73Cry_sQQ8Hnv2XikRI'
Enter fullscreen mode Exit fullscreen mode
  • Vice-versa, log in with a user that has the role ADMIN, then call GetAdminWeatherusing the received access token—you should get the weather records. However, if you try to access GetUserWeather, you should receive a 403 Forbiddenstatus. Here is the curl command:
curl --location 'http://localhost:8080/api/weatherforecast/admin-weather' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIyZjk4ZjAwMC1kYWUwLTQzODMtODNjMy0yZDczMGI3MTRkYzQiLCJ1c2VyRW1haWwiOiJ0ZXN0YWRtaW5AZXhhbXBsZS5jb20iLCJ1c2VyQWdlIjoiMzAiLCJ1c2VyUm9sZSI6IkFETUlOIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQURNSU4iLCJqdGkiOiJjNjliZjQ5NS00ZGNhLTQ3NmYtYjVlYS02M2EzOWIzOTRhNzkiLCJuYmYiOjE3MzgzMjYyNTQsImV4cCI6MTczODMyOTg1NCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiYXVkIjoibXktd2ViLWFwaS1jbGllbnQifQ.z5f2m9Iy__TbP5bpoEoGqXM5es9q-ur1gmXH31VetFI'
Enter fullscreen mode Exit fullscreen mode

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

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

Don't forget to register this new extension method in Program.cs:

//register the authorization policies
builder.Services.AddAuthorizationPolicies();
Enter fullscreen mode Exit fullscreen mode

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

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 curlcommand for this:

curl --location 'http://localhost:8080/api/weatherforecast/premium-weather' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJmYzAwYzkxMy0zYTk3LTRlYmQtYjQ2ZC1hNTQ0MjIwZjkwMjUiLCJ1c2VyRW1haWwiOiJ0ZXN0dXNlcjFAZXhhbXBsZS5jb20iLCJ1c2VyQWdlIjoiMTYiLCJ1c2VyUm9sZSI6IlVTRVIiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJVU0VSIiwianRpIjoiZGVhYzRjYmMtNGMwNS00NGM3LWI2ZGUtZWRhMGJiYTNmNTBiIiwiYWNjZXNzVG9QcmVtaXVtIjoiYWNjZXNzVG9QcmVtaXVtIiwibmJmIjoxNzM4MzI2MzA4LCJleHAiOjE3MzgzMjk5MDgsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCIsImF1ZCI6Im15LXdlYi1hcGktY2xpZW50In0.IluKD1N5jveY1xMYrejbODfBMTH7nvOCUkrAtFf57m0'
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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)