Skip to main content

Authorization & Audit (A3)

Excalibur.A3 provides a unified Authentication, Authorization, and Audit (A3) system that integrates with the Dispatch pipeline. It supports activity-based authorization, fine-grained grants, token validation, and structured audit events.

Before You Start

  • .NET 8.0+ (or .NET 9/10 for latest features)
  • Install the required packages:
    dotnet add package Excalibur.A3
    dotnet add package Excalibur.A3.Abstractions
  • Familiarity with Dispatch pipeline and security concepts

Packages

PackagePurpose
Excalibur.A3Authorization service, policies, grants, audit publisher
Excalibur.A3.AbstractionsProvider-neutral interfaces: ITokenValidator, IAuditSink, IAuditEvent, IAuthorizationEvaluator, Grant

Setup

Register A3 services via IDispatchBuilder or IServiceCollection:

using Microsoft.Extensions.DependencyInjection;

// Via IDispatchBuilder
builder.AddDispatchAuthorization();

// Or directly on IServiceCollection
services.AddDispatchAuthorization();
Single-Tenant Applications

You do not need to configure a tenant to use A3. When you call AddExcaliburA3Services(), it automatically registers ITenantId with the default value "Default" (via TenantDefaults.DefaultTenantId). All tenant-scoped features — grants, authorization policies, audit logging — work transparently.

For multi-tenant applications that serve multiple tenants from a single instance, use the factory overload:

// Resolve tenant per-request — A3 won't override it (TryAdd semantics)
services.TryAddTenantId(sp =>
{
var httpContext = sp.GetRequiredService<IHttpContextAccessor>().HttpContext;
return httpContext?.Request.Headers["X-Tenant-ID"].FirstOrDefault()
?? TenantDefaults.DefaultTenantId;
});
services.AddExcaliburA3Services(SupportedDatabase.Postgres);

Authentication

Token Validation

Implement ITokenValidator to validate tokens from any provider (JWT, opaque, API keys):

using Excalibur.A3.Authentication;

public class JwtTokenValidator : ITokenValidator
{
public async Task<AuthenticationResult> ValidateAsync(
string token,
CancellationToken cancellationToken)
{
// Validate JWT and extract claims
var principal = new AuthenticatedPrincipal(
SubjectId: "user-123",
TenantId: "tenant-abc",
Claims: new Dictionary<string, string>
{
["role"] = "admin",
["email"] = "[email protected]"
});

return new AuthenticationResult(Succeeded: true, Principal: principal);
}
}

Access Token

IAccessToken unifies authentication and authorization into a single object that combines IAuthenticationToken and IAuthorizationPolicy:

using Excalibur.A3;

// IAccessToken provides both identity and authorization checks
IAccessToken token = ...;

// Authentication
string userId = token.UserId;
string tenantId = token.TenantId;

// Authorization
bool canCreate = token.IsAuthorized("Orders.Create");
bool hasGrant = token.HasGrant<CreateOrderActivity>();

Authorization

Activity-Based Authorization

Actions that require authorization implement IRequireAuthorization:

using Excalibur.A3.Authorization;

public class CreateOrderAction : IRequireAuthorization
{
public string ActivityName => "Orders.Create";
public Guid OrderId { get; set; }
public decimal Amount { get; set; }
}

For actions that carry an access token, implement IAmAuthorizable:

using Excalibur.A3.Authorization.Requests;

public class DeleteOrderAction : IAmAuthorizable
{
public string ActivityName => "Orders.Delete";
public IAccessToken? AccessToken { get; set; }
public Guid OrderId { get; set; }
}

Authorization Policies

IAuthorizationPolicy provides tenant-scoped, activity-based authorization checks:

using Excalibur.A3.Authorization;

// Check authorization against a policy
IAuthorizationPolicy policy = ...;

// Is the user authorized for this activity?
bool authorized = policy.IsAuthorized("Orders.Create");

// Does the user have a specific grant?
bool hasGrant = policy.HasGrant("Orders.Create");

// Type-safe grant check
bool hasTypedGrant = policy.HasGrant<CreateOrderActivity>();

// Resource-scoped grant check
bool hasResourceGrant = policy.HasGrant("Order", orderId.ToString());

Authorization Service

IDispatchAuthorizationService evaluates authorization using ASP.NET Core IAuthorizationRequirement and named policies:

using Excalibur.A3.Authorization;

public class OrderHandler : IActionHandler<CreateOrderAction>
{
private readonly IDispatchAuthorizationService _authService;

public OrderHandler(IDispatchAuthorizationService authService)
{
_authService = authService;
}

public async Task HandleAsync(CreateOrderAction action, CancellationToken ct)
{
// Check against requirements
var result = await _authService.AuthorizeAsync(
user, resource: null, new OrderCreationRequirement());

// Or check against a named policy
var policyResult = await _authService.AuthorizeAsync(
user, resource: null, "OrderCreationPolicy");

if (!result.IsAuthorized)
{
throw new UnauthorizedAccessException();
}
}
}

Authorization Evaluator

For provider-neutral evaluation, implement IAuthorizationEvaluator:

using Excalibur.A3.Authorization;

public class CustomEvaluator : IAuthorizationEvaluator
{
public async Task<AuthorizationDecision> EvaluateAsync(
AuthorizationSubject subject,
AuthorizationAction action,
AuthorizationResource resource,
CancellationToken cancellationToken)
{
// Evaluate subject + action + resource triple
return new AuthorizationDecision(AuthorizationEffect.Allow);
}
}

Grants

Grants are fine-grained permissions assigned to users, scoped by tenant and resource.

Grant Model

using Excalibur.A3.Authorization.Grants;

// Grant is an event-sourced aggregate
var grant = new Grant(
userId: "user-123",
fullName: "John Doe",
tenantId: "tenant-abc",
grantType: "activity-group",
qualifier: "OrderManagement",
expiresOn: DateTimeOffset.UtcNow.AddDays(90),
grantedBy: "admin-456",
grantedOn: DateTimeOffset.UtcNow);

Managing Grants

Use dispatch commands to add and revoke grants:

var correlationId = Guid.NewGuid();

// Add a grant
await dispatcher.DispatchAsync(new AddGrantCommand(
userId: "user-123",
fullName: "John Doe",
grantType: "activity-group",
qualifier: "OrderManagement",
expiresOn: DateTimeOffset.UtcNow.AddDays(90),
correlationId: correlationId,
tenantId: "tenant-abc"), cancellationToken);

// Revoke a specific grant
await dispatcher.DispatchAsync(new RevokeGrantCommand(
userId: "user-123",
grantType: "activity-group",
qualifier: "OrderManagement",
correlationId: correlationId,
tenantId: "tenant-abc"), cancellationToken);

// Revoke all grants for a user
await dispatcher.DispatchAsync(new RevokeAllGrantsCommand(
userId: "user-123",
fullName: "John Doe",
correlationId: correlationId,
tenantId: "tenant-abc"), cancellationToken);

Grant Events

Grant changes emit domain events for audit trails:

  • GrantAdded / IGrantAdded - Emitted when a grant is created
  • GrantRevoked / IGrantRevoked - Emitted when a grant is revoked

Audit

Audit Events

IAuditEvent captures structured audit data:

using Excalibur.A3.Abstractions.Auditing;

var auditEvent = new AuditEvent(
timestampUtc: DateTimeOffset.UtcNow,
tenantId: "tenant-abc",
actorId: "user-123",
action: "CreateOrder",
resource: "Order/order-456",
outcome: "Success",
correlationId: correlationId,
attributes: new Dictionary<string, string>
{
["amount"] = "99.99",
["currency"] = "USD"
});

Audit Sink

Implement IAuditSink to persist audit events to your chosen store:

using Excalibur.A3.Abstractions.Auditing;

public class SqlAuditSink : IAuditSink
{
public async ValueTask WriteAsync(
IAuditEvent auditEvent,
CancellationToken cancellationToken)
{
// Persist to database, send to log aggregator, etc.
}
}

Audit Message Publisher

IAuditMessagePublisher publishes audit messages to external systems:

using Excalibur.A3.Audit;

public class KafkaAuditPublisher : IAuditMessagePublisher
{
public async Task PublishAsync<TMessage>(
TMessage message,
IActivityContext context,
CancellationToken cancellationToken)
{
// Publish audit event to Kafka, Azure Event Hub, etc.
}
}

What's Next

See Also