Skip to content

Authorization in ConnectSoft Microservice Template

Purpose & Overview

Authorization in the ConnectSoft Microservice Template provides a comprehensive framework for controlling access to resources and operations based on the authenticated identity's permissions, roles, claims, and contextual attributes. Authorization determines what authenticated users and services can do, enforcing security policies and ensuring least-privilege access.

Authorization in the template provides:

  • Policy-Based Authorization: Declarative, reusable authorization policies
  • Role-Based Access Control (RBAC): Permission assignment through roles
  • Attribute-Based Access Control (ABAC): Context-aware authorization decisions
  • Claims-Based Authorization: Fine-grained control using token claims
  • Scope-Based Authorization: OAuth2 scope-based permission checks
  • Resource-Based Authorization: Resource ownership and access control
  • Custom Requirements: Extensible authorization handlers
  • Multi-Tenant Authorization: Tenant-aware access control
  • Integration with Authentication: Seamless use of authenticated identity

Authorization Philosophy

Authorization determines what an authenticated identity can do. It evaluates policies based on roles, claims, attributes, and context to make access control decisions. The template supports multiple authorization models (RBAC, ABAC, policy-based) to accommodate different security requirements while maintaining consistency and testability. All authorization is declarative, policy-driven, and enforced at multiple layers (gateway, service, domain) to ensure defense in depth.

Architecture Overview

Authorization Flow

Authenticated Request
Authorization Middleware
    ├── Policy Evaluation
    ├── Requirement Evaluation
    └── Handler Execution
Authorization Models
    ├── RBAC (Roles)
    ├── ABAC (Attributes)
    ├── Claims-Based
    ├── Scope-Based
    └── Resource-Based
Authorization Decision
    ├── Allow
    └── Deny (403 Forbidden)
Resource Access

Authorization in Clean Architecture

API Layer (REST/gRPC/GraphQL)
    ├── [Authorize] Attributes
    ├── Policy Requirements
    └── Authorization Middleware
Application Layer (DomainModel)
    ├── Authorization Checks
    ├── Permission Validation
    └── Context-Based Rules
Domain Layer (Domain)
    ├── Business Rule Enforcement
    └── Domain-Level Authorization
Infrastructure Layer
    ├── Policy Handlers
    ├── Authorization Services
    └── Permission Stores

Key Integration Points

Layer Component Responsibility
ServiceModel Controllers/Endpoints Authorization attributes
ApplicationModel AuthorizationExtensions Policy configuration
ApplicationModel AuthorizationMiddleware Policy evaluation
Application Authorization Handlers Custom requirement logic
Domain Business Rules Domain-level authorization

Authorization Models

Role-Based Access Control (RBAC)

Purpose: Authorization based on roles assigned to users.

Use Cases: - Simple permission models - Administrative access control - Hierarchical role structures - Team-based access

Configuration:

// AuthorizationExtensions.cs
public static IServiceCollection AddMicroserviceAuthorization(
    this IServiceCollection services,
    IConfiguration configuration)
{
    services.AddAuthorizationBuilder()
        .AddPolicy("RequireAdmin", policy =>
        {
            policy.RequireRole("Admin");
            policy.RequireAuthenticatedUser();
        })
        .AddPolicy("RequireManager", policy =>
        {
            policy.RequireRole("Admin", "Manager");
            policy.RequireAuthenticatedUser();
        })
        .AddPolicy("RequireUser", policy =>
        {
            policy.RequireRole("Admin", "Manager", "User");
            policy.RequireAuthenticatedUser();
        });

    return services;
}

Usage:

// Controller-level authorization
[Authorize(Roles = "Admin")]
public class AdminController : ControllerBase
{
    [HttpGet("users")]
    public IActionResult GetUsers()
    {
        // Only admins can access
        return Ok();
    }
}

// Action-level authorization
[Authorize(Policy = "RequireManager")]
[HttpGet("reports")]
public IActionResult GetReports()
{
    // Managers and admins can access
    return Ok();
}

// Multiple roles
[Authorize(Roles = "Admin,Manager")]
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
    // Admins or managers can delete
    return NoContent();
}

Role Hierarchy:

// Hierarchical roles (Admin includes Manager includes User)
services.AddAuthorizationBuilder()
    .AddPolicy("RequireAdmin", policy =>
    {
        policy.RequireRole("Admin");
        policy.RequireAuthenticatedUser();
    })
    .AddPolicy("RequireManager", policy =>
    {
        // Managers and Admins (Admin is higher in hierarchy)
        policy.RequireAssertion(context =>
        {
            var roles = context.User.FindAll(ClaimTypes.Role)
                .Select(c => c.Value);

            return roles.Contains("Admin") || roles.Contains("Manager");
        });
    });

Claims-Based Authorization

Purpose: Authorization based on specific claims in the token.

Use Cases: - Fine-grained permissions - Custom claim-based access - Feature flags in claims - Tenant-specific permissions

Configuration:

// AuthorizationExtensions.cs
services.AddAuthorizationBuilder()
    .AddPolicy("CanEdit", policy =>
    {
        policy.RequireClaim("permission", "edit");
    })
    .AddPolicy("CanDelete", policy =>
    {
        policy.RequireClaim("permission", "delete");
    })
    .AddPolicy("CanManageUsers", policy =>
    {
        policy.RequireClaim("permission", "manage_users");
    })
    .AddPolicy("RequireEmailVerified", policy =>
    {
        policy.RequireClaim("email_verified", "true");
    });

Usage:

[Authorize(Policy = "CanEdit")]
[HttpPut("{id}")]
public IActionResult Update(int id, UpdateModel model)
{
    // Only users with "edit" permission can access
    return Ok();
}

// Multiple claims
[Authorize(Policy = "CanManageUsers")]
[HttpPost("users")]
public IActionResult CreateUser(CreateUserModel model)
{
    // Only users with "manage_users" permission can access
    return CreatedAtAction(nameof(GetUser), new { id = model.Id }, model);
}

// Check claims in code
[Authorize]
[HttpGet("profile")]
public IActionResult GetProfile()
{
    var emailVerified = User.HasClaim("email_verified", "true");
    if (!emailVerified)
    {
        return BadRequest("Email not verified");
    }

    return Ok();
}

Custom Claims Evaluation:

services.AddAuthorizationBuilder()
    .AddPolicy("RequirePremiumSubscription", policy =>
    {
        policy.RequireAssertion(context =>
        {
            var subscriptionTier = context.User.FindFirst("subscription_tier")?.Value;
            return subscriptionTier == "Premium" || subscriptionTier == "Enterprise";
        });
    });

Scope-Based Authorization

Purpose: Authorization based on OAuth2 scopes.

Use Cases: - OAuth2/OIDC integration - API access control - Granular API permissions - Third-party integrations

Configuration:

// AuthorizationExtensions.cs
services.AddAuthorizationBuilder()
    .AddPolicy("ReadOrders", policy =>
    {
        policy.RequireClaim("scope", "orders.read");
    })
    .AddPolicy("WriteOrders", policy =>
    {
        policy.RequireClaim("scope", "orders.write");
    })
    .AddPolicy("ManageOrders", policy =>
    {
        policy.RequireClaim("scope", "orders.read", "orders.write", "orders.delete");
    });

Scope Handler:

// ScopeAuthorizationHandler.cs
public class ScopeAuthorizationHandler : AuthorizationHandler<ScopeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        ScopeRequirement requirement)
    {
        var scopes = context.User.FindAll("scope")
            .Select(c => c.Value)
            .ToList();

        // Check if user has required scope
        if (scopes.Any(scope => requirement.AllowedScopes.Contains(scope)))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

// ScopeRequirement.cs
public class ScopeRequirement : IAuthorizationRequirement
{
    public ScopeRequirement(params string[] allowedScopes)
    {
        this.AllowedScopes = allowedScopes;
    }

    public string[] AllowedScopes { get; }
}

Usage:

[Authorize(Policy = "ReadOrders")]
[HttpGet]
public IActionResult GetOrders()
{
    // Only users with orders.read scope can access
    return Ok();
}

[Authorize(Policy = "WriteOrders")]
[HttpPost]
public IActionResult CreateOrder(CreateOrderModel model)
{
    // Only users with orders.write scope can access
    return CreatedAtAction(nameof(GetOrder), new { id = model.Id }, model);
}

Attribute-Based Access Control (ABAC)

Purpose: Authorization based on attributes (user attributes, resource attributes, environment attributes).

Use Cases: - Context-aware access control - Multi-tenant authorization - Resource ownership checks - Time-based access - Location-based access

Custom Requirement:

// ResourceOwnerRequirement.cs
public class ResourceOwnerRequirement : IAuthorizationRequirement
{
    public ResourceOwnerRequirement()
    {
    }
}

// ResourceOwnerAuthorizationHandler.cs
public class ResourceOwnerAuthorizationHandler
    : AuthorizationHandler<ResourceOwnerRequirement, IResourceWithOwner>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        ResourceOwnerRequirement requirement,
        IResourceWithOwner resource)
    {
        var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        var userRoles = context.User.FindAll(ClaimTypes.Role)
            .Select(c => c.Value)
            .ToList();

        // Admin can access any resource
        if (userRoles.Contains("Admin"))
        {
            context.Succeed(requirement);
            return Task.CompletedTask;
        }

        // User can access their own resources
        if (resource.OwnerId == userId)
        {
            context.Succeed(requirement);
            return Task.CompletedTask;
        }

        return Task.CompletedTask;
    }
}

// IResourceWithOwner.cs
public interface IResourceWithOwner
{
    string OwnerId { get; }
}

Registration:

services.AddScoped<IAuthorizationHandler, ResourceOwnerAuthorizationHandler>();

services.AddAuthorizationBuilder()
    .AddPolicy("ResourceOwner", policy =>
    {
        policy.Requirements.Add(new ResourceOwnerRequirement());
    });

Usage:

[Authorize(Policy = "ResourceOwner")]
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(int id)
{
    var order = await this.orderRepository.GetByIdAsync(id);
    if (order == null)
    {
        return NotFound();
    }

    // Authorization handler checks if user owns the order
    var authResult = await this.authorizationService.AuthorizeAsync(
        User,
        order,
        "ResourceOwner");

    if (!authResult.Succeeded)
    {
        return Forbid();
    }

    return Ok(order);
}

Multi-Attribute Authorization:

// TenantAccessRequirement.cs
public class TenantAccessRequirement : IAuthorizationRequirement
{
    public TenantAccessRequirement(string? requiredTenantId = null)
    {
        this.RequiredTenantId = requiredTenantId;
    }

    public string? RequiredTenantId { get; }
}

// TenantAccessAuthorizationHandler.cs
public class TenantAccessAuthorizationHandler
    : AuthorizationHandler<TenantAccessRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        TenantAccessRequirement requirement)
    {
        var userTenantId = context.User.FindFirst("tenant_id")?.Value;
        var userRoles = context.User.FindAll(ClaimTypes.Role)
            .Select(c => c.Value)
            .ToList();

        // Platform admin can access any tenant
        if (userRoles.Contains("PlatformAdmin"))
        {
            context.Succeed(requirement);
            return Task.CompletedTask;
        }

        // Check tenant access
        if (requirement.RequiredTenantId == null)
        {
            // No tenant requirement, allow if user has tenant
            if (!string.IsNullOrEmpty(userTenantId))
            {
                context.Succeed(requirement);
            }
        }
        else
        {
            // User must belong to required tenant
            if (userTenantId == requirement.RequiredTenantId)
            {
                context.Succeed(requirement);
            }
        }

        return Task.CompletedTask;
    }
}

Policy-Based Authorization

Purpose: Declarative, reusable authorization policies combining multiple requirements.

Use Cases: - Complex authorization rules - Reusable authorization logic - Combining multiple requirements - Business rule enforcement

Complex Policy:

// AuthorizationExtensions.cs
services.AddAuthorizationBuilder()
    .AddPolicy("CanManageOrders", policy =>
    {
        // Require authentication
        policy.RequireAuthenticatedUser();

        // Require role
        policy.RequireRole("Admin", "Manager");

        // Require claim
        policy.RequireClaim("permission", "manage_orders");

        // Require scope
        policy.RequireClaim("scope", "orders.write");

        // Custom requirement
        policy.Requirements.Add(new BusinessHoursRequirement());

        // Custom assertion
        policy.RequireAssertion(context =>
        {
            var emailVerified = context.User.HasClaim("email_verified", "true");
            var accountActive = context.User.HasClaim("account_status", "active");

            return emailVerified && accountActive;
        });
    });

Custom Requirement:

// BusinessHoursRequirement.cs
public class BusinessHoursRequirement : IAuthorizationRequirement
{
    public TimeSpan StartTime { get; }
    public TimeSpan EndTime { get; }

    public BusinessHoursRequirement(TimeSpan startTime, TimeSpan endTime)
    {
        this.StartTime = startTime;
        this.EndTime = endTime;
    }
}

// BusinessHoursAuthorizationHandler.cs
public class BusinessHoursAuthorizationHandler
    : AuthorizationHandler<BusinessHoursRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        BusinessHoursRequirement requirement)
    {
        var currentTime = DateTime.UtcNow.TimeOfDay;
        var userRoles = context.User.FindAll(ClaimTypes.Role)
            .Select(c => c.Value)
            .ToList();

        // Admins can access outside business hours
        if (userRoles.Contains("Admin"))
        {
            context.Succeed(requirement);
            return Task.CompletedTask;
        }

        // Check if within business hours
        if (currentTime >= requirement.StartTime && currentTime <= requirement.EndTime)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

Policy Composition:

// Base policies
services.AddAuthorizationBuilder()
    .AddPolicy("Authenticated", policy =>
    {
        policy.RequireAuthenticatedUser();
    })
    .AddPolicy("EmailVerified", policy =>
    {
        policy.RequireClaim("email_verified", "true");
    })
    .AddPolicy("ActiveAccount", policy =>
    {
        policy.RequireClaim("account_status", "active");
    });

// Composite policy
services.AddAuthorizationBuilder()
    .AddPolicy("VerifiedUser", policy =>
    {
        // Combine multiple policies
        policy.CombinePolicies("Authenticated", "EmailVerified", "ActiveAccount");
    });

Authorization Attributes

[Authorize] Attribute

Basic Usage:

// Require authentication
[Authorize]
public class OrdersController : ControllerBase
{
    // All actions require authentication
}

// Allow anonymous (override)
[Authorize]
public class OrdersController : ControllerBase
{
    [AllowAnonymous]
    [HttpGet("public")]
    public IActionResult GetPublicOrders()
    {
        // No authentication required
        return Ok();
    }
}

[Authorize(Roles = "...")]

[Authorize(Roles = "Admin")]
public class AdminController : ControllerBase
{
    // Only admins can access
}

// Multiple roles (OR logic)
[Authorize(Roles = "Admin,Manager")]
public class ReportsController : ControllerBase
{
    // Admins OR managers can access
}

[Authorize(Policy = "...")]

[Authorize(Policy = "RequireManager")]
public class ReportsController : ControllerBase
{
    // Policy-based authorization
}

// Multiple policies (AND logic)
[Authorize(Policy = "RequireManager")]
[Authorize(Policy = "CanViewReports")]
public class DetailedReportsController : ControllerBase
{
    // Must satisfy both policies
}

[Authorize(AuthenticationSchemes = "...")]

[Authorize(AuthenticationSchemes = "Bearer")]
public class ApiController : ControllerBase
{
    // Specific authentication scheme required
}

[Authorize(AuthenticationSchemes = "Bearer,ApiKey")]
public class HybridController : ControllerBase
{
    // Multiple authentication schemes allowed
}

Authorization in Application Layer

Manual Authorization Checks

// IAuthorizationService usage
public class OrderProcessor : IOrderProcessor
{
    private readonly IAuthorizationService authorizationService;
    private readonly IUserContext userContext;

    public OrderProcessor(
        IAuthorizationService authorizationService,
        IUserContext userContext)
    {
        this.authorizationService = authorizationService;
        this.userContext = userContext;
    }

    public async Task<ProcessOrderOutput> ProcessAsync(ProcessOrderInput input)
    {
        var order = await this.orderRepository.GetByIdAsync(input.OrderId);
        if (order == null)
        {
            throw new NotFoundException("Order not found");
        }

        // Check authorization
        var authResult = await this.authorizationService.AuthorizeAsync(
            this.userContext.Principal,
            order,
            "ResourceOwner");

        if (!authResult.Succeeded)
        {
            throw new ForbiddenException("Not authorized to access this order");
        }

        // Process order
        return await this.ProcessOrderAsync(order);
    }
}

Permission-Based Authorization

// PermissionChecker.cs
public interface IPermissionChecker
{
    Task<bool> HasPermissionAsync(string permission);
    Task<bool> HasAnyPermissionAsync(params string[] permissions);
    Task<bool> HasAllPermissionsAsync(params string[] permissions);
}

public class PermissionChecker : IPermissionChecker
{
    private readonly IAuthorizationService authorizationService;
    private readonly IUserContext userContext;

    public async Task<bool> HasPermissionAsync(string permission)
    {
        var requirement = new PermissionRequirement(permission);
        var authResult = await this.authorizationService.AuthorizeAsync(
            this.userContext.Principal,
            requirement);

        return authResult.Succeeded;
    }

    public async Task<bool> HasAnyPermissionAsync(params string[] permissions)
    {
        foreach (var permission in permissions)
        {
            if (await this.HasPermissionAsync(permission))
            {
                return true;
            }
        }

        return false;
    }

    public async Task<bool> HasAllPermissionsAsync(params string[] permissions)
    {
        foreach (var permission in permissions)
        {
            if (!await this.HasPermissionAsync(permission))
            {
                return false;
            }
        }

        return true;
    }
}

Multi-Tenant Authorization

Tenant Isolation

// TenantAuthorizationHandler.cs
public class TenantAuthorizationHandler
    : AuthorizationHandler<TenantRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        TenantRequirement requirement)
    {
        var userTenantId = context.User.FindFirst("tenant_id")?.Value;
        var resourceTenantId = requirement.TenantId;
        var userRoles = context.User.FindAll(ClaimTypes.Role)
            .Select(c => c.Value)
            .ToList();

        // Platform admin can access any tenant
        if (userRoles.Contains("PlatformAdmin"))
        {
            context.Succeed(requirement);
            return Task.CompletedTask;
        }

        // Tenant must match
        if (userTenantId == resourceTenantId)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

// TenantRequirement.cs
public class TenantRequirement : IAuthorizationRequirement
{
    public TenantRequirement(string tenantId)
    {
        this.TenantId = tenantId;
    }

    public string TenantId { get; }
}

Usage:

[Authorize]
[HttpGet("{tenantId}/orders")]
public async Task<IActionResult> GetOrders(string tenantId)
{
    // Check tenant authorization
    var authResult = await this.authorizationService.AuthorizeAsync(
        User,
        new TenantRequirement(tenantId));

    if (!authResult.Succeeded)
    {
        return Forbid();
    }

    var orders = await this.orderRepository.GetByTenantIdAsync(tenantId);
    return Ok(orders);
}

Authorization Options

Options Configuration

// AuthorizationOptions.cs
namespace ConnectSoft.MicroserviceTemplate.Options
{
    using System.ComponentModel.DataAnnotations;

    public sealed class AuthorizationOptions
    {
        public const string SectionName = "Authorization";

        public string DefaultPolicy { get; set; } = "RequireAuthenticated";
        public string FallbackPolicy { get; set; } = "Deny";

        public List<PolicyConfiguration> Policies { get; set; } = new();
    }

    public sealed class PolicyConfiguration
    {
        [Required]
        required public string Name { get; set; }

        public List<string> Roles { get; set; } = new();
        public List<ClaimRequirement> Claims { get; set; } = new();
        public List<string> Scopes { get; set; } = new();
    }

    public sealed class ClaimRequirement
    {
        [Required]
        required public string Type { get; set; }

        [Required]
        required public List<string> Values { get; set; }
    }
}

appsettings.json

{
  "Authorization": {
    "DefaultPolicy": "RequireAuthenticated",
    "FallbackPolicy": "Deny",
    "Policies": [
      {
        "Name": "RequireAdmin",
        "Roles": ["Admin"]
      },
      {
        "Name": "RequireManager",
        "Roles": ["Admin", "Manager"]
      },
      {
        "Name": "CanEdit",
        "Claims": [
          {
            "Type": "permission",
            "Values": ["edit"]
          }
        ]
      },
      {
        "Name": "ReadOrders",
        "Scopes": ["orders.read"]
      }
    ]
  }
}

Middleware Configuration

Program.cs Setup

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add authorization services
builder.Services.AddMicroserviceAuthorization(builder.Configuration);

// Add authentication (required for authorization)
builder.Services.AddMicroserviceAuthentication(builder.Configuration);

var app = builder.Build();

// Configure middleware pipeline
app.UseRouting();

// Authentication must be before authorization
app.UseAuthentication();

// Authorization middleware
app.UseAuthorization();

app.MapControllers();

await app.RunAsync();

Default and Fallback Policies

services.AddAuthorization(options =>
{
    // Default policy (applied when [Authorize] has no parameters)
    options.DefaultPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();

    // Fallback policy (applied when no [Authorize] attribute)
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();

    // Or deny all by default
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAssertion(_ => false)
        .Build();
});

Best Practices

Do's

  1. Use Policy-Based Authorization

    // ✅ GOOD - Reusable, testable policy
    [Authorize(Policy = "RequireManager")]
    
    // ❌ BAD - Hardcoded role check
    if (User.IsInRole("Manager")) { ... }
    

  2. Enforce Authorization at Multiple Layers

    // ✅ GOOD - API layer + Application layer
    [Authorize(Policy = "ResourceOwner")]
    public async Task<IActionResult> GetOrder(int id)
    {
        // Also check in application layer
        var authResult = await this.authorizationService.AuthorizeAsync(
            User, order, "ResourceOwner");
    }
    

  3. Use Least Privilege

    // ✅ GOOD - Specific permission
    [Authorize(Policy = "CanEditOrders")]
    
    // ❌ BAD - Too broad
    [Authorize(Roles = "Admin")]
    

  4. Validate Resource Ownership

    // ✅ GOOD - Check ownership
    var authResult = await this.authorizationService.AuthorizeAsync(
        User, resource, "ResourceOwner");
    
    // ❌ BAD - Trust user input
    var resource = await this.repository.GetByIdAsync(userId);
    

  5. Use Tenant Isolation

    // ✅ GOOD - Tenant-aware authorization
    var authResult = await this.authorizationService.AuthorizeAsync(
        User, new TenantRequirement(tenantId));
    
    // ❌ BAD - No tenant check
    var orders = await this.repository.GetAllAsync();
    

  6. Log Authorization Failures

    // ✅ GOOD - Log for security monitoring
    if (!authResult.Succeeded)
    {
        this.logger.LogWarning(
            "Authorization failed for user {UserId} accessing resource {ResourceId}",
            userId, resourceId);
        return Forbid();
    }
    

Don'ts

  1. Don't Rely Only on Client-Side Authorization

    // ❌ BAD - Client-side only
    // JavaScript: if (user.role === 'Admin') { ... }
    
    // ✅ GOOD - Server-side authorization
    [Authorize(Roles = "Admin")]
    

  2. Don't Skip Authorization Checks

    // ❌ BAD - No authorization
    [HttpGet]
    public IActionResult GetOrders()
    {
        return Ok(this.orders);
    }
    
    // ✅ GOOD - Authorized
    [Authorize]
    [HttpGet]
    public IActionResult GetOrders()
    {
        return Ok(this.orders);
    }
    

  3. Don't Use String Literals for Roles

    // ❌ BAD - Magic strings
    [Authorize(Roles = "Admin")]
    
    // ✅ GOOD - Constants
    public static class Roles
    {
        public const string Admin = "Admin";
        public const string Manager = "Manager";
    }
    
    [Authorize(Roles = Roles.Admin)]
    

  4. Don't Expose Authorization Details

    // ❌ BAD - Reveals internal structure
    return BadRequest($"User {userId} lacks permission {permission}");
    
    // ✅ GOOD - Generic message
    return Forbid();
    

  5. Don't Bypass Authorization in Tests

    // ❌ BAD - Skip authorization in tests
    // Test setup that bypasses authorization
    
    // ✅ GOOD - Mock authorization properly
    var mockAuth = new Mock<IAuthorizationService>();
    mockAuth.Setup(x => x.AuthorizeAsync(...))
        .ReturnsAsync(AuthorizationResult.Success());
    

Testing

Unit Testing Authorization

[TestMethod]
public async Task Authorize_WithValidRole_ShouldSucceed()
{
    // Arrange
    var claims = new List<Claim>
    {
        new Claim(ClaimTypes.NameIdentifier, "user-123"),
        new Claim(ClaimTypes.Role, "Admin")
    };
    var identity = new ClaimsIdentity(claims, "Test");
    var principal = new ClaimsPrincipal(identity);

    var authService = new AuthorizationService(
        new Mock<IAuthorizationPolicyProvider>().Object,
        new[] { new Mock<IAuthorizationHandler>().Object });

    var policy = new AuthorizationPolicyBuilder()
        .RequireRole("Admin")
        .Build();

    // Act
    var result = await authService.AuthorizeAsync(principal, null, policy);

    // Assert
    Assert.IsTrue(result.Succeeded);
}

Integration Testing

[TestMethod]
public async Task GetOrders_WithoutAuthorization_ShouldReturnForbidden()
{
    // Arrange
    var client = this.factory.CreateClient();
    // No authentication token

    // Act
    var response = await client.GetAsync("/api/orders");

    // Assert
    Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode);
}

[TestMethod]
public async Task GetOrders_WithAdminRole_ShouldReturnOk()
{
    // Arrange
    var client = this.factory.CreateClient();
    var token = await this.CreateTokenAsync(new[] { "Admin" });
    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue("Bearer", token);

    // Act
    var response = await client.GetAsync("/api/orders");

    // Assert
    Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}

Troubleshooting

Issue: Authorization Always Fails

Symptoms: All requests return 403 Forbidden.

Solutions: - Verify authentication is working (check User.Identity.IsAuthenticated) - Check policy requirements match user's claims/roles - Verify authorization middleware is after authentication middleware - Check policy registration in AddAuthorization

Issue: Roles Not Recognized

Symptoms: Users with roles still get 403 Forbidden.

Solutions: - Verify role claim type matches (usually ClaimTypes.Role or "role") - Check role names match exactly (case-sensitive) - Verify roles are in token claims - Check token scopes include role information

Issue: Policy Not Applied

Symptoms: Authorization attribute doesn't enforce policy.

Solutions: - Verify policy name matches exactly - Check policy is registered in AddAuthorization - Verify [Authorize] attribute is on controller or action - Check middleware order (authentication before authorization)

Issue: Resource-Based Authorization Fails

Symptoms: Resource ownership checks always fail.

Solutions: - Verify resource implements required interface - Check authorization handler is registered - Verify user ID claim matches resource owner ID - Check tenant ID matches if using tenant isolation

Summary

Authorization in the ConnectSoft Microservice Template provides:

  • Policy-Based: Declarative, reusable authorization policies
  • Multiple Models: RBAC, ABAC, Claims-based, Scope-based
  • Flexible Requirements: Custom authorization handlers
  • Multi-Tenant Support: Tenant-aware authorization
  • Clean Architecture: Framework-agnostic authorization checks
  • Defense in Depth: Authorization at multiple layers
  • Testable: Easy to unit test and integration test
  • Extensible: Custom requirements and handlers

By implementing authorization, teams can:

  • Control Access: Enforce who can access what resources
  • Enforce Policies: Apply business rules through authorization
  • Support Multi-Tenancy: Isolate tenants with authorization
  • Maintain Security: Defense in depth through layered authorization
  • Test Authorization: Verify authorization logic works correctly
  • Scale Policies: Reusable policies across services

Authorization is the enforcement layer that ensures authenticated identities can only access resources and perform actions they are permitted to, protecting the microservice from unauthorized access and maintaining data security and privacy.