ASP.NET Core 8 Web API -

Authorization

Authorization is a crucial aspect of securing your ASP.NET Core 8 Web API, ensuring that authenticated users have the necessary permissions to access specific resources. This guide covers role-based authorization, policy-based authorization, custom authorization handlers, and best practices.


1. Introduction to Authorization

Authorization is the process of determining what an authenticated user is allowed to do. It ensures that users can only access the resources and perform the actions they are permitted to.


2. Authentication vs Authorization

Authentication: The process of verifying the identity of a user or service. It answers the question, "Who are you?"

Authorization: The process of determining what an authenticated user is allowed to do. It answers the question, "What can you do?"

Note: Authentication is always performed before authorization. Once the identity of the user is confirmed, authorization policies determine what resources the user can access.


3. Role-Based Authorization

Role-based authorization uses roles to control access to resources. Users are assigned roles, and those roles dictate what they can do.

3.1 Configure Role-Based Authorization in Program.cs
        
            
var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddControllers();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
        };
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminPolicy", policy => policy.RequireRole("Admin"));
});

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();
app.Run();

        
    

3.2 Protecting an Endpoint by Role Example:
        
            
[Authorize(Roles = "Admin")]
[ApiController]
[Route("api/[controller]")]
public class AdminController : ControllerBase
{
    [HttpGet]
    public IActionResult GetAdminData()
    {
        var data = new List<string> { "Admin Data1", "Admin Data2" };
        return Ok(data);
    }
}

        
    

4. Policy-Based Authorization

Policy-based authorization allows for more complex access control scenarios by defining policies with requirements and handlers.

4.1 Configure Policy-Based Authorization in Program.cs
        
            
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminPolicy", policy => policy.RequireRole("Admin"));
    options.AddPolicy("Over18Policy", policy => policy.Requirements.Add(new MinimumAgeRequirement(18)));
});

builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();

        
    

4.2 Define a Custom Requirement and Handler Custom Requirement:
        
            
public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }

    public MinimumAgeRequirement(int minimumAge)
    {
        MinimumAge = minimumAge;
    }
}

        
    

Custom Handler:
        
            
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
    {
        if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth))
        {
            return Task.CompletedTask;
        }

        var dateOfBirth = Convert.ToDateTime(context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth).Value);

        int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
        if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
        {
            calculatedAge--;
        }

        if (calculatedAge >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

        
    

4.3 Protecting an Endpoint by Policy Example:
        
            
[Authorize(Policy = "Over18Policy")]
[ApiController]
[Route("api/[controller]")]
public class RestrictedController : ControllerBase
{
    [HttpGet]
    public IActionResult GetRestrictedData()
    {
        var data = new List<string> { "Restricted Data1", "Restricted Data2" };
        return Ok(data);
    }
}

        
    

5. Custom Authorization Handlers

Custom authorization handlers allow you to implement complex authorization logic.

5.1 Create a Custom Handler Custom Handler:
        
            
public class CustomAuthorizationHandler : AuthorizationHandler<CustomRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomRequirement requirement)
    {
        // Custom authorization logic
        if (context.User.HasClaim(c => c.Type == "CustomClaim" && c.Value == requirement.AllowedValue))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

        
    

Custom Requirement:
        
            
public class CustomRequirement : IAuthorizationRequirement
{
    public string AllowedValue { get; }

    public CustomRequirement(string allowedValue)
    {
        AllowedValue = allowedValue;
    }
}

        
    

5.2 Register Custom Handler in Program.cs
        
            
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CustomPolicy", policy => policy.Requirements.Add(new CustomRequirement("AllowedValue")));
});

builder.Services.AddSingleton<IAuthorizationHandler, CustomAuthorizationHandler>();

        
    

5.3 Protecting an Endpoint by Custom Policy Example:
        
            
[Authorize(Policy = "CustomPolicy")]
[ApiController]
[Route("api/[controller]")]
public class CustomAuthController : ControllerBase
{
    [HttpGet]
    public IActionResult GetCustomData()
    {
        var data = new List<string> { "Custom Data1", "Custom Data2" };
        return Ok(data);
    }
}

        
    

6. Claims-Based Authorization

Claims-based authorization checks the value of specific claims and grants access based on those claims.

6.1 Add Claims to JWT Token
        
            
public string GenerateJwtToken(string username, string role, DateTime dateOfBirth)
{
    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

    var claims = new[]
    {
        new Claim(JwtRegisteredClaimNames.Sub, username),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        new Claim(ClaimTypes.Role, role),
        new Claim(ClaimTypes.DateOfBirth, dateOfBirth.ToString("yyyy-MM-dd"))
    };

    var token = new JwtSecurityToken(
        issuer: _configuration["Jwt:Issuer"],
        audience: _configuration["Jwt:Audience"],
        claims: claims,
        expires: DateTime.Now.AddMinutes(30),
        signingCredentials: credentials
    );

    return new JwtSecurityTokenHandler().WriteToken(token);
}

        
    

6.2 Protecting an Endpoint by Claims Example:
        
            
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class ClaimsController : ControllerBase
{
    [HttpGet]
    [Authorize(Policy = "Over18Policy")]
    public IActionResult GetClaimsData()
    {
        var data = new List<string> { "Claims Data1", "Claims Data2" };
        return Ok(data);
    }
}

        
    

7. Best Practices for Authorization


8. Comprehensive Example

Program.cs:
        
            
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddControllers();

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        ValidAudience = builder.Configuration["Jwt:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
    };
});

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminPolicy", policy => policy.RequireRole("Admin"));
    options.AddPolicy("Over18Policy", policy => policy.Requirements.Add(new MinimumAgeRequirement(18)));
    options.AddPolicy("CustomPolicy", policy => policy.Requirements.Add(new CustomRequirement("AllowedValue")));
});

builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, CustomAuthorizationHandler>();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();
app.Run();

        
    


AuthService:
        
            
public class AuthService
{
    private readonly IConfiguration _configuration;

    public AuthService(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public string GenerateJwtToken(string username, string role, DateTime dateOfBirth)
    {
        var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
        var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub, username),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new Claim(ClaimTypes.Role, role),
            new Claim(ClaimTypes.DateOfBirth, dateOfBirth.ToString("yyyy-MM-dd"))
        };

        var token = new JwtSecurityToken(
            issuer: _configuration["Jwt:Issuer"],
            audience: _configuration["Jwt:Audience"],
            claims: claims,
            expires: DateTime.Now.AddMinutes(30),
            signingCredentials: credentials
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

        
    


MinimumAgeRequirement and Handler:
        
            
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)
    {
        if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth))
        {
            return Task.CompletedTask;
        }

        var dateOfBirth = Convert.ToDateTime(context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth).Value);

        int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
        if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
        {
            calculatedAge--;
        }

        if (calculatedAge >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

        
    


CustomRequirement and Handler:
        
            
public class CustomRequirement : IAuthorizationRequirement
{
    public string AllowedValue { get; }

    public CustomRequirement(string allowedValue)
    {
        AllowedValue = allowedValue;
    }
}

public class CustomAuthorizationHandler : AuthorizationHandler<CustomRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == "CustomClaim" && c.Value == requirement.AllowedValue))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

        
    


AdminController:
        
            
[Authorize(Roles = "Admin")]
[ApiController]
[Route("api/[controller]")]
public class AdminController : ControllerBase
{
    [HttpGet]
    public IActionResult GetAdminData()
    {
        var data = new List<string> { "Admin Data1", "Admin Data2" };
        return Ok(data);
    }
}

        
    


RestrictedController:
        
            
[Authorize(Policy = "Over18Policy")]
[ApiController]
[Route("api/[controller]")]
public class RestrictedController : ControllerBase
{
    [HttpGet]
    public IActionResult GetRestrictedData()
    {
        var data = new List<string> { "Restricted Data1", "Restricted Data2" };
        return Ok(data);
    }
}

        
    


CustomAuthController:
        
            
[Authorize(Policy = "CustomPolicy")]
[ApiController]
[Route("api/[controller]")]
public class CustomAuthController : ControllerBase
{
    [HttpGet]
    public IActionResult GetCustomData()
    {
        var data = new List<string> { "Custom Data1", "Custom Data2" };
        return Ok(data);
    }
}

        
    

9. Conclusion

Authorization is a vital part of securing your ASP.NET Core 8 Web API. By implementing role-based and policy-based authorization, as well as custom authorization handlers, you can ensure that only authorized users can access specific resources. Following best practices such as using secure policies, centralizing authorization logic, and regularly reviewing roles and policies will help you build secure and reliable web APIs. This comprehensive guide provides the tools and knowledge to implement effective authorization in your ASP.NET Core 8 Web API projects.