Skip to content

Authorization

Beacon provides a flexible, pluggable authorization system that supports both simple role-based access control (RBAC) and fine-grained resource-level permissions.

The authorization system consists of:

  • IBeaconUserContext - Provides access to current user information
  • IBeaconAuthorizationProvider - Enforces authorization policies
  • IBeaconAuthenticationProvider - Pluggable authentication (the sample uses DatabaseAuthenticationProvider)
  • Built-in Providers - Ready-to-use implementations (Default, Role-Based, Database-Backed)
  • Custom Providers - Plug in your own authorization logic
  • Opt-in by default — authorization is disabled unless explicitly enabled
  • Pluggable architecture — integrate with any authentication system
  • Cookie-based sessions — a login form is served at the React route /login; optional OIDC/SSO and JWT bearer (for MCP) are also supported
  • Multiple authorization levels — global permissions and resource-level permissions
  • Backward compatible — existing installations work unchanged
  • Framework agnostic — works with ASP.NET Core Identity, OAuth, custom auth, etc.
  • Database-backed roles — built-in user management with Admin, Editor, Viewer roles

Update your Program.cs to enable authorization:

builder.Services.AddBeaconServices(builder.Configuration, options =>
{
options.AddBeaconScheduler<BeaconScheduler>();
options.BaseUrl = "https://localhost:7187";
// Enable authorization
options.Authorization.Enabled = true;
options.AddAuthorizationProvider<RoleBasedAuthorizationProvider>();
})
.UsePostgreSql(connectionString, "beacon");
// Serves the React SPA (Beacon.UI Razor Class Library) at the root URL "/"
builder.Services.AddBeaconUI();
var app = builder.Build();
// Authentication + authorization middleware
app.UseAuthentication();
app.UseAuthorization();
// Map the React shell + Beacon REST API
app.UseBeaconUI();

The built-in RoleBasedAuthorizationProvider requires role claims. Add a claims transformer:

using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Beacon.Core.Authorization;
public class MyClaimsTransformation : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var identity = (ClaimsIdentity)principal.Identity!;
// Add Beacon role claim
identity.AddClaim(new Claim(BeaconClaims.Role, "Admin"));
identity.AddClaim(new Claim(BeaconClaims.UserId, principal.Identity.Name));
identity.AddClaim(new Claim(BeaconClaims.UserName, principal.Identity.Name));
return Task.FromResult(principal);
}
}
// Register it
builder.Services.AddScoped<IClaimsTransformation, MyClaimsTransformation>();

Start your application and verify:

  • Username appears in the React UI (not hardcoded)
  • Unauthorized operations return 403 Forbidden
  • Different roles have different permissions

Inject IBeaconUserContext anywhere in your application:

public class MyService
{
private readonly IBeaconUserContext _userContext;
public MyService(IBeaconUserContext userContext)
{
_userContext = userContext;
}
public void DoSomething()
{
var userId = _userContext.UserId;
var userName = _userContext.UserName;
var email = _userContext.Email;
var isAuthenticated = _userContext.IsAuthenticated;
if (_userContext.HasClaim(BeaconClaims.Role, "Admin"))
{
// Admin-only logic
}
}
}

Use these standard claim types for consistency:

BeaconClaims.UserId // "beacon:user_id"
BeaconClaims.UserName // "beacon:user_name"
BeaconClaims.Role // "beacon:role"
BeaconClaims.Permission // "beacon:permission"

Allows all operations (backward compatible default).

Use case: When you don’t need authorization or handle it elsewhere.

// Authorization disabled (default behavior)
builder.Services.AddBeaconServices(builder.Configuration, options =>
{
// Authorization.Enabled = false by default
options.AddBeaconScheduler<BeaconScheduler>();
})
.UsePostgreSql(connectionString, "beacon");

Simple RBAC with three built-in roles:

RoleReadWriteDeleteExecuteArchive
Admin
Editor
Viewer
Guest

Use case: Simple role-based permissions without complex logic.

options.Authorization.Enabled = true;
options.AddAuthorizationProvider<RoleBasedAuthorizationProvider>();

Required claims:

identity.AddClaim(new Claim(BeaconClaims.Role, "Admin")); // or "Editor", "Viewer"

Database-backed authorization that reads roles from Beacon’s user management tables. This is the recommended provider when using the built-in User Management system.

RoleLevelReadCreate/Edit/ExecuteDelete/Archive
Admin3YesYesYes
Editor2YesYesNo
Viewer1YesNoNo

Use case: When using Beacon’s built-in user management with login form and role assignment.

options.Authorization.Enabled = true;
// Enable user management + database auth
options.Authentication.EnableLoginForm = true;
options.AddAuthenticationProvider<DatabaseAuthenticationProvider>();
options.UserManagement = new UserManagementOptions
{
Enabled = true,
AllowInternalUsers = true
};

No claims transformer needed — roles are loaded directly from the database.

Implement IBeaconAuthorizationProvider to create custom authorization logic:

using Beacon.Core.Authorization;
public class MyAuthorizationProvider : IBeaconAuthorizationProvider
{
private readonly IBeaconUserContext _userContext;
private readonly IMyPermissionService _permissionService;
public MyAuthorizationProvider(
IBeaconUserContext userContext,
IMyPermissionService permissionService)
{
_userContext = userContext;
_permissionService = permissionService;
}
// Global permissions (required)
public async Task<bool> HasReadPermissionAsync(
CancellationToken cancellationToken = default)
{
return await _permissionService.HasPermissionAsync(
_userContext.UserId,
"beacon.read");
}
public async Task<bool> HasWritePermissionAsync(
CancellationToken cancellationToken = default)
{
return await _permissionService.HasPermissionAsync(
_userContext.UserId,
"beacon.write");
}
// Resource-level permissions (optional - return null to skip)
public async Task<AuthorizationResult?> AuthorizeAsync(
ResourceType resourceType,
int resourceId,
PermissionAction action,
CancellationToken cancellationToken = default)
{
// Example: Check resource ownership
if (resourceType == ResourceType.Query)
{
var query = await _dbContext.Queries.FindAsync(resourceId);
if (query.CreatedByUserId == _userContext.UserId)
return AuthorizationResult.Success();
return AuthorizationResult.Failure("Not the owner");
}
// Return null to use global permissions only
return null;
}
public Task<AuthorizationResult?> AuthorizeNewResourceAsync(
ResourceType resourceType,
PermissionAction action,
object? resourceContext = null,
CancellationToken cancellationToken = default)
{
// Check if user can create new resources
return Task.FromResult<AuthorizationResult?>(null);
}
public async Task<IEnumerable<int>?> GetAccessibleResourceIdsAsync(
ResourceType resourceType,
PermissionAction action,
CancellationToken cancellationToken = default)
{
// Return null = user sees all
// Return empty list = user sees nothing
// Return specific IDs = user sees only those
if (resourceType == ResourceType.DataSource)
{
// Return only data sources user has access to
return await _dbContext.DataSources
.Where(ds => ds.CreatedByUserId == _userContext.UserId)
.Select(ds => ds.Id)
.ToListAsync(cancellationToken);
}
return null; // No filtering
}
}

Register your provider:

options.Authorization.Enabled = true;
options.AddAuthorizationProvider<MyAuthorizationProvider>();
// Add Identity
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
// Add Beacon with authorization
builder.Services.AddBeaconServices(builder.Configuration, options =>
{
options.Authorization.Enabled = true;
options.AddAuthorizationProvider<RoleBasedAuthorizationProvider>();
})
.UsePostgreSql(connectionString, "beacon");
// Claims transformer
public class IdentityToBeaconClaimsTransformer : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var identity = (ClaimsIdentity)principal.Identity!;
// Map Identity roles to Beacon roles
if (principal.IsInRole("Administrator"))
identity.AddClaim(new Claim(BeaconClaims.Role, "Admin"));
else if (principal.IsInRole("PowerUser"))
identity.AddClaim(new Claim(BeaconClaims.Role, "Editor"));
else
identity.AddClaim(new Claim(BeaconClaims.Role, "Viewer"));
// Add user claims
identity.AddClaim(new Claim(BeaconClaims.UserId,
principal.FindFirstValue(ClaimTypes.NameIdentifier)));
identity.AddClaim(new Claim(BeaconClaims.UserName,
principal.Identity.Name));
return Task.FromResult(principal);
}
}
builder.Services.AddScoped<IClaimsTransformation, IdentityToBeaconClaimsTransformer>();
// Add authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = "https://your-identity-provider.com";
options.ClientId = "your-client-id";
options.ClientSecret = "your-client-secret";
options.ResponseType = "code";
options.SaveTokens = true;
});
// Add Beacon with authorization
builder.Services.AddBeaconServices(builder.Configuration, options =>
{
options.Authorization.Enabled = true;
options.AddAuthorizationProvider<RoleBasedAuthorizationProvider>();
})
.UsePostgreSql(connectionString, "beacon");
// Claims transformer
public class OAuthToBeaconClaimsTransformer : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var identity = (ClaimsIdentity)principal.Identity!;
// Map OAuth roles to Beacon roles
var role = principal.FindFirstValue("role") switch
{
"admin" => "Admin",
"user" => "Editor",
_ => "Viewer"
};
identity.AddClaim(new Claim(BeaconClaims.Role, role));
identity.AddClaim(new Claim(BeaconClaims.UserId,
principal.FindFirstValue("sub")));
identity.AddClaim(new Claim(BeaconClaims.UserName,
principal.FindFirstValue("name")));
return Task.FromResult(principal);
}
}
// Your external auth service
public interface IExternalAuthService
{
Task<bool> CheckPermissionAsync(string userId, string permission);
Task<string[]> GetUserRolesAsync(string userId);
}
// Custom authorization provider
public class ExternalAuthProvider : IBeaconAuthorizationProvider
{
private readonly IBeaconUserContext _userContext;
private readonly IExternalAuthService _externalAuth;
public ExternalAuthProvider(
IBeaconUserContext userContext,
IExternalAuthService externalAuth)
{
_userContext = userContext;
_externalAuth = externalAuth;
}
public async Task<bool> HasReadPermissionAsync(
CancellationToken cancellationToken = default)
{
return await _externalAuth.CheckPermissionAsync(
_userContext.UserId,
"beacon.read");
}
public async Task<bool> HasWritePermissionAsync(
CancellationToken cancellationToken = default)
{
return await _externalAuth.CheckPermissionAsync(
_userContext.UserId,
"beacon.write");
}
// Implement other methods...
}
// Register it
builder.Services.AddScoped<IExternalAuthService, YourExternalAuthService>();
options.AddAuthorizationProvider<ExternalAuthProvider>();
public enum ResourceType
{
DataSource = 1, // Database connections
Query = 2, // SQL queries
QueryFolder = 3, // Query organization
Subscription = 4, // Scheduled query executions
Recipient = 5, // Notification recipients
QueryTask = 6, // Manual query tasks
MigrationJob = 7, // Data migration jobs
DataSourceDocumentation = 8, // AI-generated documentation
AiActor = 9, // AI monitoring agents
AiActorPlan = 10, // AI actor execution plans
AiAlertConfiguration = 11 // AI-generated alerts
}
public enum PermissionAction
{
Read = 1, // View resource
Create = 2, // Create new resource
Update = 3, // Modify existing resource
Delete = 4, // Permanently delete resource
Execute = 5, // Execute query/subscription
Archive = 6, // Archive resource (soft delete)
Approve = 7, // Approve AI Actor plans
Lock = 8, // Lock query from AI modifications
Export = 9 // Export documentation/data
}

Implement fine-grained permissions based on resource ownership:

public async Task<AuthorizationResult?> AuthorizeAsync(
ResourceType resourceType,
int resourceId,
PermissionAction action,
CancellationToken cancellationToken = default)
{
// Allow admins to do anything
if (_userContext.HasClaim(BeaconClaims.Role, "Admin"))
return AuthorizationResult.Success();
// Check ownership for queries
if (resourceType == ResourceType.Query)
{
var query = await _dbContext.Queries
.Where(q => q.Id == resourceId)
.Select(q => new { q.CreatedByUserId, q.IsShared })
.FirstOrDefaultAsync(cancellationToken);
if (query == null)
return AuthorizationResult.Failure("Query not found");
// Owner has full access
if (query.CreatedByUserId == _userContext.UserId)
return AuthorizationResult.Success();
// Others can only read if shared
if (query.IsShared && action == PermissionAction.Read)
return AuthorizationResult.Success();
return AuthorizationResult.Failure("Access denied");
}
return null; // Use global permissions
}

Cache permissions for better performance:

public class CachedAuthorizationProvider : IBeaconAuthorizationProvider
{
private readonly IBeaconUserContext _userContext;
private readonly IMemoryCache _cache;
private readonly IActualAuthProvider _actualProvider;
public async Task<bool> HasWritePermissionAsync(
CancellationToken cancellationToken = default)
{
var cacheKey = $"write_perm_{_userContext.UserId}";
return await _cache.GetOrCreateAsync(cacheKey, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
return await _actualProvider.HasWritePermissionAsync(cancellationToken);
});
}
}

Filter resources by tenant:

public async Task<IEnumerable<int>?> GetAccessibleResourceIdsAsync(
ResourceType resourceType,
PermissionAction action,
CancellationToken cancellationToken = default)
{
var tenantId = _userContext.Metadata["TenantId"] as string;
if (resourceType == ResourceType.DataSource)
{
return await _dbContext.DataSources
.Where(ds => ds.TenantId == tenantId)
.Select(ds => ds.Id)
.ToListAsync(cancellationToken);
}
return null;
}

Optional audit fields are available on entities for future audit trail support:

public abstract class AuditableBaseEntity : BaseEntity
{
public string? CreatedByUserId { get; set; }
public string? CreatedByUserName { get; set; }
public DateTime? ModifiedTime { get; set; }
public string? ModifiedByUserId { get; set; }
public string? ModifiedByUserName { get; set; }
}

Note: These fields are currently nullable and not automatically populated. A future release will include an EF Core interceptor to populate these fields automatically when authorization is enabled.

public class AuthorizationOptions
{
/// <summary>
/// Enable authorization checks. Default: false
/// </summary>
public bool Enabled { get; set; } = false;
/// <summary>
/// Authorization provider type. If null, uses DefaultAuthorizationProvider.
/// </summary>
public Type? ProviderType { get; set; }
/// <summary>
/// Enable resource-level authorization (requires provider support).
/// Default: false (use global read/write only)
/// </summary>
public bool EnableResourceLevelAuthorization { get; set; } = false;
}
builder.Services.AddBeaconServices(builder.Configuration, options =>
{
options.AddBeaconScheduler<BeaconScheduler>();
options.BaseUrl = "https://localhost:7187";
// Authorization configuration
options.Authorization.Enabled = true;
options.Authorization.EnableResourceLevelAuthorization = true;
options.AddAuthorizationProvider<MyAuthorizationProvider>();
})
.UsePostgreSql(connectionString, "beacon");

Problem: Users can access resources they shouldn’t.

Solution:

  1. Verify authorization is enabled:

    options.Authorization.Enabled = true;
  2. Verify UseAuthorization() is called:

    app.UseAuthentication();
    app.UseAuthorization(); // ← This must be present
    app.UseBeaconUI(); // serves the React SPA at "/"
  3. Check claims are added correctly:

    // Add logging to your claims transformer
    _logger.LogInformation("User {User} assigned role {Role}",
    principal.Identity.Name, role);

Problem: The React UI shows “Guest” or nothing.

Solution:

  1. Verify IBeaconUserContext is registered (automatic with AddBeaconUI())
  2. Check that the claims transformer adds BeaconClaims.UserName
  3. Verify the user is authenticated

Problem: All requests return 403 Forbidden.

Solution:

  1. Check the authorization provider is returning true for authenticated users
  2. Add logging to the authorization provider:
    _logger.LogWarning("Authorization denied for {User}: {Reason}",
    _userContext.UserName, result.FailureReason);
  3. Verify the claims transformer is executed

Problem: Authorization provider methods never execute.

Solution:

  1. Verify the provider is registered:
    options.AddAuthorizationProvider<MyAuthorizationProvider>();
  2. Check authorization is enabled
  3. Ensure UseAuthorization() middleware is added
  1. Always validate on the server - Client-side checks are for UX only
  2. Use HTTPS in production - Protect credentials and session cookies
  3. Store role assignments in the database - Don’t hardcode in the claims transformer
  4. Implement rate limiting - Prevent brute force attacks
  5. Audit authorization failures - Log all 403 responses with context
  6. Use strong session management - Implement proper timeout and renewal
  7. Validate resource ownership - Don’t rely on resource IDs alone
  8. Principle of least privilege - Default to the most restrictive permissions

If you are upgrading from a version without authorization:

  1. No changes required - Authorization is disabled by default
  2. Opt-in when ready - Enable authorization when you’re ready
  3. No breaking changes - Existing code continues to work
  4. Gradual adoption - Start with global permissions, add resource-level later