<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alex Mutegi</title>
    <description>The latest articles on DEV Community by Alex Mutegi (@kiipz).</description>
    <link>https://dev.to/kiipz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2602125%2F9b872738-da26-411d-9028-81fd9f0a0775.png</url>
      <title>DEV Community: Alex Mutegi</title>
      <link>https://dev.to/kiipz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kiipz"/>
    <language>en</language>
    <item>
      <title>Permission-Based Authorization in ASP.NET Core: A Step-by-Step Guide</title>
      <dc:creator>Alex Mutegi</dc:creator>
      <pubDate>Sun, 22 Dec 2024 18:30:15 +0000</pubDate>
      <link>https://dev.to/kiipz/-2n2i</link>
      <guid>https://dev.to/kiipz/-2n2i</guid>
      <description>&lt;div class="ltag__link"&gt;
  &lt;a href="/kiipz" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2602125%2F9b872738-da26-411d-9028-81fd9f0a0775.png" alt="kiipz"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/kiipz/permission-based-authorization-in-aspnet-core-a-step-by-step-guide-1c3o" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Permission-Based Authorization in ASP.NET Core: A Step-by-Step Guide&lt;/h2&gt;
      &lt;h3&gt;Alex Mutegi ・ Dec 22&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#csharp&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#aspnet&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#api&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#web&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
      <category>dotnet</category>
      <category>aspnet</category>
      <category>tutorial</category>
      <category>security</category>
    </item>
    <item>
      <title>Permission-Based Authorization in ASP.NET Core: A Step-by-Step Guide</title>
      <dc:creator>Alex Mutegi</dc:creator>
      <pubDate>Sun, 22 Dec 2024 15:27:13 +0000</pubDate>
      <link>https://dev.to/kiipz/permission-based-authorization-in-aspnet-core-a-step-by-step-guide-1c3o</link>
      <guid>https://dev.to/kiipz/permission-based-authorization-in-aspnet-core-a-step-by-step-guide-1c3o</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Authorization is a critical aspect of building secure applications. In ASP.NET Core, the built-in authorization framework can be extended to support fine-grained permission-based control. This article explains how to implement a permission-based authorization system using custom attributes, policies, and handlers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview of Permission-Based Authorization
&lt;/h2&gt;

&lt;p&gt;Permissions provide a more granular approach to controlling user access compared to role-based authorization. By associating specific actions with permissions, developers can enforce precise rules for accessing resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Components of the System
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. PermissionsEnum&lt;/strong&gt;&lt;br&gt;
The &lt;em&gt;PermissionsEnum&lt;/em&gt; defines all the permissions in the system. Each permission is assigned a unique value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public enum PermissionsEnum
{
    // Users
    UserRead = 1,
    UserCreate = 2,
    UserModify = 3,
    UserDelete = 4,
    // Roles
    RoleRead = 5,
    RoleCreate = 6,
    RoleModify = 7,
    RoleDelete = 8
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Controller with Permission Attribute&lt;/strong&gt;&lt;br&gt;
Permissions are applied to controller actions using the &lt;em&gt;HasPermission&lt;/em&gt; attribute. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[HttpGet("users")]
[HasPermission(PermissionsEnum.UserRead)]
public async Task&amp;lt;ActionResult&amp;lt;PaginatedResponse&amp;lt;GetUserDTO&amp;gt;&amp;gt;&amp;gt; GetAllUsers(int pageNumber = 1, int pageSize = 10, string search = null)
{
    var result = await _user.GetUsersAsync(pageNumber, pageSize, search);

    return Ok(result);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;em&gt;HasPermission&lt;/em&gt; attribute ensures that users must have at least one of the specified permissions to access this endpoint. ie.&lt;br&gt;
&lt;em&gt;[HasPermission(PermissionsEnum.UserRead, PermissionsEnum.UserRead)]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Custom Attribute: HasPermission&lt;/strong&gt;&lt;br&gt;
The &lt;em&gt;HasPermission&lt;/em&gt; attribute simplifies permission checks by associating multiple permissions with a policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using Domain.Enums; // specify directory for permissions enums
using Microsoft.AspNetCore.Authorization;

public sealed class HasPermissionAttribute : AuthorizeAttribute
{
    public HasPermissionAttribute(params PermissionsEnum[] permissions)
        : base(policy: string.Join(",", permissions.Select(p =&amp;gt; p.ToString())))
    {
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Policy Provider&lt;/strong&gt;&lt;br&gt;
The &lt;em&gt;PermissionAuthorizationPolicyProvider&lt;/em&gt; dynamically generates authorization policies based on permission names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;

public class PermissionAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider
{
    public PermissionAuthorizationPolicyProvider(IOptions&amp;lt;AuthorizationOptions&amp;gt; options)
        : base(options) { }

    public override async Task&amp;lt;AuthorizationPolicy?&amp;gt; GetPolicyAsync(string policyName)
    {
        var policy = await base.GetPolicyAsync(policyName);

        if (policy is not null)
            return policy;

        var permissions = policyName.Split(',');

        return new AuthorizationPolicyBuilder()
            .AddRequirements(new PermissionRequirement(permissions))
            .Build();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Authorization Handler&lt;/strong&gt;&lt;br&gt;
The &lt;em&gt;PermissionAuthorizationHandler&lt;/em&gt; evaluates if the user has any of the required permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using Microsoft.AspNetCore.Authorization;

public class PermissionAuthorizationHandler : AuthorizationHandler&amp;lt;PermissionRequirement&amp;gt;
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
    {
        var userPermissions = context.User.Claims
                                      .Where(c =&amp;gt; c.Type == "permission")
                                      .Select(c =&amp;gt; c.Value);

        if (requirement.Permissions.Any(permission =&amp;gt; userPermissions.Contains(permission)))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Integration in ASP.NET Core&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Service Configuration&lt;/strong&gt;&lt;br&gt;
Register the required services in &lt;em&gt;Program.cs&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;services.AddAuthorization();
services.AddSingleton&amp;lt;IAuthorizationHandler, PermissionAuthorizationHandler&amp;gt;();
services.AddSingleton&amp;lt;IAuthorizationPolicyProvider, PermissionAuthorizationPolicyProvider&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Adding Permissions to Users&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Permissions should be added as claims to user identities during login or token generation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public async Task&amp;lt;LoginResponse&amp;gt; LoginUserAsync(LoginDTO loginDTO)
{
    var getUser = await FindUserByEmail(loginDTO.Email!);
    if (getUser == null)
        return new LoginResponse(false, "Invalid credentials!");

    bool checkPassword = BCrypt.Net.BCrypt.Verify(loginDTO.Password, getUser.Password);
    if (checkPassword)
    {
        if (getUser.EmailVerifiedOn is null)
            return new LoginResponse(false, "Your email is not verified");

        var accessToken = await _authTokenService.GenerateToken(getUser);
        var refreshToken = _authTokenService.GenerateRefreshToken();
        _authTokenService.SetRefreshTokenAsHttpOnlyCookie(refreshToken);

        // Update database
        getUser.RefreshToken = refreshToken.TokenValue;
        getUser.RefreshTokenExpiryTime = refreshToken.Expires;
        await _appDbContext.SaveChangesAsync();

        return new LoginResponse(true, "Login successful", accessToken);
    }
    else
        return new LoginResponse(false, "Invalid credentials!");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public async Task&amp;lt;string&amp;gt; GenerateToken(User user)
{
    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]!));
    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

    var userClaims = new List&amp;lt;Claim&amp;gt;()
    {
        new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
        new Claim(JwtRegisteredClaimNames.Name, user.Name),
        new Claim(JwtRegisteredClaimNames.Email, user.Email)
    };

    foreach (var permission in await _user.GetUserPermissions(user))
        userClaims.Add(new Claim("permission", permission.Name));

    var token = new JwtSecurityToken(
        issuer: _configuration["Jwt:Issuer"],
        audience: _configuration["Jwt:Audience"],
        claims: userClaims,
        expires: DateTime.UtcNow.AddHours(1),
        signingCredentials: credentials
    );

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

public RefreshToken GenerateRefreshToken()
{
    var refreshToken = new RefreshToken
    {
        TokenValue = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)),
        Expires = DateTime.UtcNow.AddDays(7)
    };

    return refreshToken;
}

public void SetRefreshTokenAsHttpOnlyCookie(RefreshToken refreshToken)
{
    var cookieOptions = new CookieOptions
    {
        HttpOnly = true,
        Secure = true,
        SameSite = SameSiteMode.None,
        Expires = refreshToken.Expires
    };

    var httpContext = _httpContextAccessor.HttpContext;
    httpContext?.Response.Cookies.Append(_configuration["Jwt:RefreshTokenCookieKey"], refreshToken.TokenValue, cookieOptions);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Database Seeder&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;To get started, you may need to seed permissions into your database. Additionally, it’s a good practice to seed an Admin role and user when running your application for the first time. Here's how you can do it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public static class DatabaseSeeder
{
    public static async void SeedAsync(IServiceProvider serviceProvider)
    {
        using var scope = serviceProvider.CreateScope();
        var _context = scope.ServiceProvider.GetRequiredService&amp;lt;AppDbContext&amp;gt;();

        // Ensure the database is migrated
        await _context.Database.MigrateAsync();

        // 1. Seed Permissions
        var permissions = Enum.GetValues(typeof(PermissionsEnum))
                                  .Cast&amp;lt;PermissionsEnum&amp;gt;()
                                  .Select(e =&amp;gt; new Permission { Name = e.ToString() })
                                  .ToList(); // Get permissions from Enum File

        var permissionToAdd = new List&amp;lt;Permission&amp;gt;();

        foreach (var permission in permissions)
        {
            if (!_context.Permissions.Any(p =&amp;gt; p.Name == permission.Name))
            {
                permissionToAdd.Add(new Permission
                {
                    Name = permission.Name,
                });
            }
        }

        await _context.AddRangeAsync(permissionToAdd);
        await _context.SaveChangesAsync();

        // 2.Seed Admin Role
        var adminRole = new Role { Name = "Admin" };

        if (!_context.Roles.Any(r =&amp;gt; r.Name == adminRole.Name))
        {
            _context.Roles.Add(adminRole);
            await _context.SaveChangesAsync();
        }
        else
        {
            adminRole = _context.Roles.FirstOrDefault(r =&amp;gt; r.Name == adminRole.Name);
        }

        // 3. Assign all permissions to Admin role
        var storedPermissions = await _context.Permissions.ToListAsync();
        foreach (var permission in storedPermissions)
        {
            if (!_context.RolePermissions.Any(rp =&amp;gt; rp.RoleId == adminRole.Id &amp;amp;&amp;amp; rp.PermissionId == permission.Id))
            {
                _context.RolePermissions.Add(new RolePermission
                {
                    RoleId = adminRole.Id,
                    PermissionId = permission.Id
                });
            }
        }
        await _context.SaveChangesAsync();

        // 4. Seed Admin user
        var adminUser = new User
        {
            Name = "Alex Mutegi",
            Email = "username@example.com",
            Password = BCrypt.Net.BCrypt.HashPassword("MyStrongPassword"),
            EmailVerifiedOn = DateTime.UtcNow
        };

        if (!_context.Users.Any(u =&amp;gt; u.Email == adminUser.Email))
        {
            _context.Users.Add(adminUser);
            await _context.SaveChangesAsync();
        }
        else
        {
            adminUser = _context.Users.FirstOrDefault(u =&amp;gt; u.Email == adminUser.Email);
        }

        // 5. Assign Admin Role to Admin User
        if (!_context.UserRoles.Any(ur =&amp;gt; ur.UserId == adminUser.Id &amp;amp;&amp;amp; ur.RoleId == adminRole.Id))
        {
            _context.UserRoles.Add(new UserRole
            {
                UserId = adminUser.Id,
                RoleId = adminRole.Id
            });
        }

        await _context.SaveChangesAsync();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the Program.cs, register the seeder like below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var app = builder.Build();
DatabaseSeeder.SeedAsync(app.Services);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Database diagram might look like the below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0xe14g0ktp19i5yznl18.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0xe14g0ktp19i5yznl18.png" alt="Image description" width="800" height="662"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;With this setup, your ASP.NET Core application now supports a robust permission-based authorization system. By leveraging the flexibility of custom attributes, policy providers, and handlers, you can enforce fine-grained control over your application’s resources.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>aspnet</category>
      <category>api</category>
      <category>web</category>
    </item>
  </channel>
</rss>
