Authorization Filters in ASP.NET Core Web API

Authorization Filters in ASP.NET Core Web API

In this article, I will discuss Authorization Filters in ASP.NET Core Web API Applications with Examples. Please read our previous article discussing the basic concepts of Filters in ASP.NET Core Web API Applications.

What is an Authorization Filter?

An Authorization filter controls access to certain parts of our application. In ASP.NET Core, authorization filters verify whether the user has the appropriate rights to perform the requested action. Authorization filters run before other action filters and are responsible for allowing or denying access to certain endpoints based on the user’s credentials and claims.

It is responsible for ensuring the current user is authorized to execute the action. If the user is not authorized, the filter prevents the action method from executing and returns an appropriate response (typically a 401 Unauthorized or 403 Forbidden status code). These filters are responsible for enforcing authorization policies defined in the application.

What is Authentication?

Authentication is the process of verifying the identity of a user or system. It answers the question, “Who are you?”. This is usually done using Common methods such as:

  • Username and Password: The most basic form of authentication.
  • OAuth/OpenID Connect: Protocols for token-based authentication.
  • Multi-Factor Authentication (MFA): Combines multiple verification methods.

In other words, authentication is a process of validating someone against a data source. Let’s understand Authentication from a layman’s point of view. For this, please have a look at the following diagram.

What is Authentication?

The above image shows the different sections of an IT Company, such as Reception, HR Section, Accounts Section, Server Room, etc. At the gate, we have biometrics to verify the employee. Suppose one employee comes. This biometrics checks the employee credentials against some data source, and if it is found that the employee is valid, it only allows entering into the campus. This is nothing but Authentication.

What is Authorization?

Authorization is the process of verifying what actions or resources the authenticated user is allowed to access, i.e., whether an authenticated user has the right permissions to perform a specific action. It answers the question: What are you allowed to do? Once the user is authenticated, their access level is determined based on their roles or claims. The following are the two approaches to implement Authorization:

  • Role-Based Authorization: Grants access based on user roles (e.g., Admin, User).
  • Policy-Based Authorization: Uses policies to define more granular access controls.

The most important point to remember is that authentication happens first, followed by authorization. For a better understanding, please have a look at the following image.

What is Authorization?

As shown in the above image, once the employee is authenticated, he enters the Campus. Then, Authorization comes into the picture. Within the campus, which section he is allowed to enter is determined by the Authorization process. The Role of the Employee does this. If the Employee has list privileges, he may not allow each section. On the other hand, if the Employee has the highest privileges, he may be allowed to enter each section.

How Do We Implement Authorization Filters in ASP.NET Core Web API?

By default, in the ASP.NET Core Web API applications, all the action methods of all controllers can be accessed by both authenticated and anonymous users. However, if we want the action methods to be available only for authenticated and authorized users, we need to use the Authorization Filter in ASP.NET Core MVC. ASP.NET Core provides two built-in attributes, [Authorize] and [AllowAnonymous], that can be used as Authorization filters.

[Authorize]:

This attribute is used to enforce authorization rules on controllers or action methods. When we use this attribute, the action or controller is only accessible to authenticated users who meet specific criteria (roles, policies, claims). Example: [Authorize], [Authorize(Roles = “Admin”)], [Authorize(Policy = “CanEdit”)]

[AllowAnonymous]:

This attribute allows users to access a particular controller or action without being authorized, even if the [Authorize] attribute has been applied at a higher level (globally or to the entire controller). Example: [AllowAnonymous].

Real-Time Example of Using Authorization Filters in ASP.NET Core Web API

Let’s create a sample ASP.NET Core Web API with a controller that has secured and unsecured endpoints using the [Authorize] and [AllowAnonymous] attributes. We will also apply filters at the Global, Controller, and Action levels. In this example, I will use JWT Authentication, but I will explain it in detail in our upcoming articles. For now, use JWT Authentication.

ASP.NET Core Web API Project Setup

First, create a new ASP.NET Core Web API Project with the name AuthorizationFilterDemo. For simplicity, we will use JWT (JSON Web Tokens) for authentication. So, please install the JwtBearer NuGet Package, which is required for JWT Authentication in an ASP.NET Core application, by executing the below command in the Package Manager Console.

Install-Package Microsoft.AspNetCore.Authentication.JwtBearer

Modifying appsettings.json file:

Please modify the appsettings.json file as follows. Here, we are adding a key that we will use to generate and validate the JWT token.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Jwt": {
    "Key": "KHPK6Ucf/zjvU4qW8/vkuuGLHeIo0l9ACJiTaAPLKbk=" //Secret Key
  }
}
Add JWT Authentication Services and Middleware Components:

Next, we need to configure JWT Authentication Services into the dependency injection container, and we also need to add the authentication middleware components to the request processing pipeline. So, please modify the Program class as follows. The following code is self-explained, so please read the comment lines for a better understanding.

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace AuthorizationFilterDemo
{
    public class Program
    {
        public static void Main(string[] args)
        {
            // Create a builder object to configure the application.
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the application's dependency injection container.
            builder.Services.AddControllers()
            .AddJsonOptions(options =>
            {
                // Configure JSON serialization to retain property names as defined in the C# model.
                // This disables the default camelCase naming policy.
                options.JsonSerializerOptions.PropertyNamingPolicy = null;
            });

            // Configure JWT authentication in the application.
            builder.Services.AddAuthentication(options =>
            {
                // Set the default authentication scheme to JWT Bearer.
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(options =>
            {
                // Configure JWT token validation parameters.
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = false, // Skip validating the token issuer (simplified for this example).
                    ValidateAudience = false, // Skip validating the token audience (simplified for this example).
                    ValidateIssuerSigningKey = true, // Ensure the token's signing key is valid.
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])) // Use a symmetric key from configuration for token validation.
                };
            });

            // Register services for Swagger, which generates API documentation.
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            // Build the application and configure middleware.
            var app = builder.Build();

            // Conditionally enable Swagger middleware for API documentation in development environments.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger(); 
                app.UseSwaggerUI(); 
            }

            // Enforce HTTPS for all requests to ensure secure communication.
            app.UseHttpsRedirection();

            // Add authentication middleware to validate JWT tokens in incoming requests.
            app.UseAuthentication();

            // Add authorization middleware to check user permissions for accessing resources.
            app.UseAuthorization();

            // Map incoming HTTP requests to controllers in the application.
            app.MapControllers();

            // Start the application and listen for incoming HTTP requests.
            app.Run();
        }
    }
}
Creating Login Model:

For generating the JWT Token, we need a user Name and Password, and for this, we need a model. So, first, create a folder named Models in the project root directory. Create a class file inside the Models folder named UserLogin.cs, then copy and paste the following code.

namespace AuthorizationFilterDemo.Models
{
    public class UserLogin
    {
        public string Username { get; set; }
        public string Password { get; set; }
    }
}
Generate JWT Tokens:

For demonstration, create a simple controller to authenticate users and generate JWT tokens. So, create a new API Empty Controller named AuthController within the Controllers folder and copy and paste the following code.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using AuthorizationFilterDemo.Models;

namespace AuthorizationFilterDemo.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class AuthController : ControllerBase
    {
        // Private field to store configuration settings, typically used for accessing appsettings.json.
        private readonly IConfiguration _config;

        // Constructor to initialize the configuration field via dependency injection.
        public AuthController(IConfiguration config)
        {
            _config = config; // Assign the injected IConfiguration instance to the private field.
        }

        // Specifies that this endpoint allows anonymous access (no authentication required).
        [AllowAnonymous]
        // Defines the route "api/auth/login" for this POST endpoint.
        [HttpPost("login")] 
        public IActionResult Login([FromBody] UserLogin userLogin)
        {
            // Validate user credentials (hardcoded here for demonstration purposes).
            if (userLogin.Username.ToLower() == "admin" && userLogin.Password.ToLower() == "password")
            {
                // Generate a JWT token for the authenticated user.
                var token = GenerateJwtToken(userLogin.Username);

                // Return a 200 OK response with the token in the response body.
                return Ok(new { Token = token });
            }

            // If credentials are invalid, return a 401 Unauthorized response.
            return Unauthorized();
        }

        // Private method to generate a JWT token for a valid user.
        private string GenerateJwtToken(string username)
        {
            // Create a symmetric security key using a secret key from the configuration.
            var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));

            // Specify the signing credentials using the security key and HMAC-SHA256 algorithm.
            var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

            // Define the claims for the JWT token, including username and role.
            var claims = new[]
            {
                new Claim(ClaimTypes.Name, username), // Claim representing the user's name.
                new Claim(ClaimTypes.Role, "Admin")   // Claim representing the user's role.
            };

            // Create a JWT token with specified claims, expiration, and signing credentials.
            var token = new JwtSecurityToken(
                issuer: null, // No specific issuer specified.
                audience: null, // No specific audience specified.
                claims: claims, // Pass the claims defined above.
                expires: DateTime.Now.AddHours(1), // Set token expiration to 1 hour from now.
                signingCredentials: credentials); // Include signing credentials for the token.

            // Serialize the JWT token into a string and return it.
            return new JwtSecurityTokenHandler().WriteToken(token);
        }
    }
}
JWT Generation:
  • Symmetric Key: A secret key is used to sign the token, which is retrieved from configuration for security.
  • Claims: Claims represent user-specific data (e.g., username and role).
  • Expiration: Tokens have a limited validity (1 hour here) to enhance security.
  • Signing Credentials: Ensures the token’s integrity using HMAC-SHA256.
Applying Filter Globally

To apply authorization globally, configure it in the Program.cs when adding controllers.

builder.Services.AddControllers(options =>
{
    options.Filters.Add(new AuthorizeFilter());
});

Now, all controllers and action methods require authorization by default. Use [AllowAnonymous] to allow access to specific actions or controllers.

Applying [Authorize] and [AllowAnonymous] Attributes at Controller and Action Level:

Now, we will create one controller with two action methods. One action method will require authentication, and another action method can be accessed by any user. So, create a new API Empty controller named ProductsController within the Controllers folder and copy and paste the following code.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace AuthorizationFilterDemo.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    [Authorize] // Controller-level authorization
    public class ProductsController : ControllerBase
    {
        // GET: api/products
        [HttpGet]
        public IActionResult GetProducts()
        {
            return Ok(new[] { "Product 1", "Product 2", "Product 3" });
        }

        // GET: api/products/public
        [HttpGet("public")]
        [AllowAnonymous] // Action-level override
        public IActionResult GetPublicProducts()
        {
            return Ok(new[] { "Public Product 1", "Public Product 2" });
        }

        // GET: api/products/admin
        [HttpGet("admin")]
        [Authorize(Roles = "Admin")] // Role-based authorization
        public IActionResult GetAdminProducts()
        {
            return Ok(new[] { "Admin Product 1", "Admin Product 2" });
        }
    }
}
Code Explanation:
  • The ProductsController is decorated with [Authorize], meaning all its actions require authentication by default.
  • The GetProducts action inherits this requirement.
  • The GetPublicProducts action is decorated with [AllowAnonymous], allowing unauthenticated access despite the controller-level [Authorize].
  • The GetAdminProducts is decorated with [Authorize(Roles = “Admin”)]. To access this method, the user must be authenticated and have the admin role.
Testing at Different Authorization Levels
  • Global Authorization: When [Authorize] is added globally, all the application’s endpoints will require authentication. You can verify this by trying to access endpoints without providing a token. For testing purposes, add the [AllowAnonymous] attribute to specific actions if you want to override the global authorization.
  • Controller-Level Authorization: [Authorize] at the controller level requires all actions within that controller to be authenticated unless explicitly overridden by [AllowAnonymous]. You can verify this by trying to access GET /api/products/public without a token, which should work, while GET /api/products should require a valid token.
  • Action-Level Authorization: [Authorize(Roles = “Admin”)] applied to the GetAdminProducts action restricts access to users with the Admin role. You can verify this by using a token without the Admin role and observing that access is denied (403 Forbidden).
Testing the Application:

To test each API endpoint of this example, you can use tools like Postman or Fiddler. Below are the steps to test each of the endpoints using Postman:

Step 1: Obtain JWT Token

Step 1: Obtain JWT Token
Endpoint: POST /api/auth/login
URL: https://localhost:{port}/api/auth/login
Body (JSON):
{
“Username”: “admin”,
“Password”: “password”
}

Expected Result: You should receive a response containing the JWT token. Copy this token as you will need it for the next requests.
{
“token”: “<JWT_TOKEN>”
}

Step 2: Test Public Endpoint

Endpoint: GET /api/products/public
URL: https://localhost:{port}/api/products/public
Headers: None required
Expected Result: This endpoint does not require authentication, and you should receive the following response:
[
“Public Product 1”,
“Public Product 2”
]

Step 3: Test Secured Endpoint (Requires Authentication)

Endpoint: GET /api/products
URL: https://localhost:{port}/api/products
Headers:
Authorization: Bearer <JWT_TOKEN> (Use the token obtained in Step 1)
Expected Result: You should receive the following response:
[
“Product 1”,
“Product 2”,
“Product 3”
]

Note: If you do not provide a valid token, you should receive an Unauthorized response.

Step 4: Test Admin-Only Endpoint (Role-Based Authorization)

Endpoint: GET /api/products/admin
URL: https://localhost:{port}/api/products/admin
Headers:
Authorization: Bearer <JWT_TOKEN> (Use the token obtained in Step 1)
Expected Result: You should receive the following response:
[
“Admin Product 1”,
“Admin Product 2”
]

Note: If the token does not have the role, Admin, you should receive a Forbid response.

When Should We Use Authorization Filters in ASP.NET Core Web API?

We should use authorization filters to enforce security policies for our API endpoints. Authorization filters are essential for:

  • Protecting Sensitive Data: Restrict access to certain controllers or actions in our Web API endpoints based on user roles or claims.
  • Role-Based Access Control: Allowing or denying access based on user roles or permissions.
  • Centralized Logic: Centralize the logic of permission checks rather than duplicating it across multiple endpoints.
  • Custom Authorization Logic: Implement custom business rules to control how users interact with your application.

In the next article, I will discuss the Resource Filters in ASP.NET Core Web API Application. In this article, I explain Authorization Filters in ASP.NET Core Web API Applications. I hope you enjoy this Authorization Filters in ASP.NET Core Web API article.

Leave a Reply

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