Skip to main content

Overview

SAPFIAI implements a sophisticated authorization system that combines:
  • Role-Based Access Control (RBAC) - Users assigned to roles
  • Permission-Based Authorization - Fine-grained permissions assigned to roles
  • Policy-Based Authorization - ASP.NET Core authorization policies
  • Claim-Based Authorization - JWT claims for authorization decisions
Authorization determines what authenticated users can do, while authentication verifies who they are.

Authorization Architecture

Permission-Based Authorization

Permission Entity

From src/Domain/Entities/Permission.cs:6:
Permission.cs
public class Permission : BaseEntity
{
    /// <summary>
    /// Unique permission name (e.g., "users.create")
    /// </summary>
    public string Name { get; set; } = string.Empty;

    /// <summary>
    /// Description of what this permission allows
    /// </summary>
    public string? Description { get; set; }

    /// <summary>
    /// Module this permission belongs to (e.g., "Users", "Reports")
    /// </summary>
    public string Module { get; set; } = string.Empty;

    /// <summary>
    /// Whether this permission is currently active
    /// </summary>
    public bool IsActive { get; set; } = true;

    /// <summary>
    /// Relationship to roles through RolePermission
    /// </summary>
    public ICollection<RolePermission> RolePermissions { get; set; } = new List<RolePermission>();
}

Permission Naming Convention

Permissions follow a hierarchical naming pattern:
{module}.{action}
Examples:
  • users.create - Create new users
  • users.read - View user information
  • users.update - Modify existing users
  • users.delete - Delete users
  • reports.view - View reports
  • settings.manage - Manage system settings
Use lowercase with dots to separate module and action for consistency and clarity.

Authorization Implementation

Permission Authorization Handler

From src/Infrastructure/Authorization/PermissionAuthorizationHandler.cs:5:
PermissionAuthorizationHandler.cs
public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, 
        PermissionRequirement requirement)
    {
        // Check if user has the required permission claim
        var permissionClaim = context.User.FindFirst(c => 
            c.Type == "permission" && 
            c.Value == requirement.Permission);

        if (permissionClaim != null)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

Permission Requirement

PermissionRequirement.cs
public class PermissionRequirement : IAuthorizationRequirement
{
    public string Permission { get; }

    public PermissionRequirement(string permission)
    {
        Permission = permission;
    }
}

Using Authorization in Endpoints

Require Authorization

From src/Web/Endpoints/Authentication.cs:71:
group.MapPost("/logout", Logout)
    .WithName("Logout")
    .WithOpenApi()
    .Produces<Result>(StatusCodes.Status200OK)
    .RequireAuthorization();  // Requires authenticated user

Require Specific Permission

group.MapGet("/audit-logs", GetAuditLogs)
    .WithName("GetAuditLogs")
    .Produces<IEnumerable<AuditLogDto>>(StatusCodes.Status200OK)
    .WithOpenApi()
    .RequireAuthorization("CanPurge");  // Requires specific permission

Require Role

group.MapPost("/admin/settings", UpdateSettings)
    .RequireAuthorization(policy => policy.RequireRole("Administrator"));

Policy-Based Authorization

Defining Authorization Policies

From src/Domain/Constants/Policies.cs:
Policies.cs
public static class Policies
{
    public const string CanPurge = nameof(CanPurge);
    public const string CanManageUsers = nameof(CanManageUsers);
    public const string CanViewReports = nameof(CanViewReports);
    public const string CanManageRoles = nameof(CanManageRoles);
}

Registering Policies

In src/Infrastructure/DependencyInjection.cs:
services.AddAuthorizationBuilder()
    .AddPolicy(Policies.CanPurge, policy =>
        policy.Requirements.Add(new PermissionRequirement("system.purge")))
    .AddPolicy(Policies.CanManageUsers, policy =>
        policy.Requirements.Add(new PermissionRequirement("users.manage")))
    .AddPolicy(Policies.CanViewReports, policy =>
        policy.Requirements.Add(new PermissionRequirement("reports.view")))
    .AddPolicy(Policies.CanManageRoles, policy =>
        policy.Requirements.Add(new PermissionRequirement("roles.manage")));

// Register the permission handler
services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();

Role-Permission Relationship

RolePermission Entity

RolePermission.cs
public class RolePermission
{
    public string RoleId { get; set; } = string.Empty;
    public int PermissionId { get; set; }

    // Navigation properties
    public IdentityRole Role { get; set; } = null!;
    public Permission Permission { get; set; } = null!;
}

Assigning Permissions to Roles

// Create command to assign permission to role
var command = new AssignPermissionToRoleCommand
{
    RoleId = "admin-role-id",
    PermissionId = 1
};

await mediator.Send(command);
AssignPermissionToRoleCommandHandler.cs
public async Task<Result> Handle(
    AssignPermissionToRoleCommand request, 
    CancellationToken cancellationToken)
{
    // Check if role exists
    var role = await _context.Roles
        .FindAsync(new object[] { request.RoleId }, cancellationToken);
    
    if (role == null)
        return Result.Failure(new[] { "Role not found" });

    // Check if permission exists
    var permission = await _context.Permissions
        .FindAsync(new object[] { request.PermissionId }, cancellationToken);
    
    if (permission == null)
        return Result.Failure(new[] { "Permission not found" });

    // Check if already assigned
    var exists = await _context.RolePermissions
        .AnyAsync(rp => 
            rp.RoleId == request.RoleId && 
            rp.PermissionId == request.PermissionId, 
            cancellationToken);

    if (exists)
        return Result.Failure(new[] { "Permission already assigned to role" });

    // Create assignment
    var rolePermission = new RolePermission
    {
        RoleId = request.RoleId,
        PermissionId = request.PermissionId
    };

    _context.RolePermissions.Add(rolePermission);
    await _context.SaveChangesAsync(cancellationToken);

    return Result.Success();
}

Authorization in Application Layer

Using IUser Interface

From src/Application/Common/Interfaces/IUser.cs:
public interface IUser
{
    string? Id { get; }
    string? Email { get; }
    bool IsAuthenticated { get; }
    bool IsInRole(string role);
    bool HasPermission(string permission);
}

Checking Authorization in Handlers

public class DeleteUserCommandHandler : IRequestHandler<DeleteUserCommand, Result>
{
    private readonly IUser _currentUser;
    private readonly IApplicationDbContext _context;

    public async Task<Result> Handle(
        DeleteUserCommand request, 
        CancellationToken cancellationToken)
    {
        // Check if user has permission
        if (!_currentUser.HasPermission("users.delete"))
        {
            throw new ForbiddenAccessException();
        }

        // Prevent users from deleting themselves
        if (_currentUser.Id == request.UserId)
        {
            return Result.Failure(new[] { "Cannot delete your own account" });
        }

        // Proceed with deletion
        // ...
    }
}

Common Authorization Patterns

Check if user owns the resource they’re trying to access:
var document = await _context.Documents
    .FirstOrDefaultAsync(d => d.Id == request.DocumentId);

if (document.OwnerId != _currentUser.Id && 
    !_currentUser.IsInRole("Administrator"))
{
    throw new ForbiddenAccessException();
}

Authorization Response Codes

Status CodeMeaningWhen to Use
401 UnauthorizedNo credentials or invalid credentialsUser not authenticated
403 ForbiddenValid credentials but insufficient permissionsUser authenticated but lacks required permission
404 Not FoundResource doesn’t exist or user can’t access itHide existence of resources user can’t access
Always return 404 instead of 403 when hiding resources from unauthorized users to prevent information disclosure.

Best Practices

  • Grant users only the permissions they absolutely need
  • Start with minimal permissions and add as needed
  • Regularly audit and remove unused permissions
  • Use temporary permission elevation for sensitive operations
  • Implement authorization at multiple layers (API, Application, Database)
  • Never rely solely on client-side authorization checks
  • Validate authorization even for internal service calls
  • Log all authorization failures for security monitoring
  • Make permissions specific enough to be useful
  • Avoid overly granular permissions that become unmanageable
  • Group related permissions into modules
  • Use hierarchical naming for easy management
  • Write tests for both positive and negative authorization cases
  • Test permission boundaries and edge cases
  • Verify that unauthorized access attempts are logged
  • Test role and permission combinations

Next Steps

Roles & Permissions

Learn how to create and manage roles and permissions

Authorization API

View the API reference for roles and permissions

Security Features

Explore audit logging for authorization events

Testing

Learn how to test authorization in your application

Build docs developers (and LLMs) love