Authorization
Beacon provides a flexible, pluggable authorization system that supports both simple role-based access control (RBAC) and fine-grained resource-level permissions.
Overview
Section titled “Overview”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
Key Features
Section titled “Key Features”- 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
Quick Start
Section titled “Quick Start”1. Enable Authorization
Section titled “1. Enable Authorization”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 middlewareapp.UseAuthentication();app.UseAuthorization();
// Map the React shell + Beacon REST APIapp.UseBeaconUI();2. Add Role Claims
Section titled “2. Add Role Claims”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 itbuilder.Services.AddScoped<IClaimsTransformation, MyClaimsTransformation>();3. Test Authorization
Section titled “3. Test Authorization”Start your application and verify:
- Username appears in the React UI (not hardcoded)
- Unauthorized operations return 403 Forbidden
- Different roles have different permissions
User Context
Section titled “User Context”Accessing Current User
Section titled “Accessing Current User”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 } }}Standard Claims
Section titled “Standard Claims”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"Built-in Authorization Providers
Section titled “Built-in Authorization Providers”DefaultAuthorizationProvider
Section titled “DefaultAuthorizationProvider”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");RoleBasedAuthorizationProvider
Section titled “RoleBasedAuthorizationProvider”Simple RBAC with three built-in roles:
| Role | Read | Write | Delete | Execute | Archive |
|---|---|---|---|---|---|
| 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"DatabaseAuthorizationProvider
Section titled “DatabaseAuthorizationProvider”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.
| Role | Level | Read | Create/Edit/Execute | Delete/Archive |
|---|---|---|---|---|
| Admin | 3 | Yes | Yes | Yes |
| Editor | 2 | Yes | Yes | No |
| Viewer | 1 | Yes | No | No |
Use case: When using Beacon’s built-in user management with login form and role assignment.
options.Authorization.Enabled = true;
// Enable user management + database authoptions.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.
Custom Authorization Provider
Section titled “Custom Authorization Provider”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>();Integration Examples
Section titled “Integration Examples”ASP.NET Core Identity
Section titled “ASP.NET Core Identity”// Add Identitybuilder.Services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>();
// Add Beacon with authorizationbuilder.Services.AddBeaconServices(builder.Configuration, options =>{ options.Authorization.Enabled = true; options.AddAuthorizationProvider<RoleBasedAuthorizationProvider>();}).UsePostgreSql(connectionString, "beacon");
// Claims transformerpublic 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>();OAuth 2.0 / OpenID Connect
Section titled “OAuth 2.0 / OpenID Connect”// Add authenticationbuilder.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 authorizationbuilder.Services.AddBeaconServices(builder.Configuration, options =>{ options.Authorization.Enabled = true; options.AddAuthorizationProvider<RoleBasedAuthorizationProvider>();}).UsePostgreSql(connectionString, "beacon");
// Claims transformerpublic 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); }}External Authorization Service
Section titled “External Authorization Service”// Your external auth servicepublic interface IExternalAuthService{ Task<bool> CheckPermissionAsync(string userId, string permission); Task<string[]> GetUserRolesAsync(string userId);}
// Custom authorization providerpublic 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 itbuilder.Services.AddScoped<IExternalAuthService, YourExternalAuthService>();options.AddAuthorizationProvider<ExternalAuthProvider>();Resource Types and Actions
Section titled “Resource Types and Actions”Resource Types
Section titled “Resource Types”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}Permission Actions
Section titled “Permission Actions”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}Advanced Scenarios
Section titled “Advanced Scenarios”Resource-Level Authorization
Section titled “Resource-Level Authorization”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}Permission Caching
Section titled “Permission Caching”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); }); }}Multi-Tenancy
Section titled “Multi-Tenancy”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;}Audit Trail (Future)
Section titled “Audit Trail (Future)”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.
Configuration Options
Section titled “Configuration Options”Authorization Options
Section titled “Authorization Options”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;}Example Configuration
Section titled “Example Configuration”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");Troubleshooting
Section titled “Troubleshooting”Authorization Not Working
Section titled “Authorization Not Working”Problem: Users can access resources they shouldn’t.
Solution:
-
Verify authorization is enabled:
options.Authorization.Enabled = true; -
Verify
UseAuthorization()is called:app.UseAuthentication();app.UseAuthorization(); // ← This must be presentapp.UseBeaconUI(); // serves the React SPA at "/" -
Check claims are added correctly:
// Add logging to your claims transformer_logger.LogInformation("User {User} assigned role {Role}",principal.Identity.Name, role);
Username Not Displayed
Section titled “Username Not Displayed”Problem: The React UI shows “Guest” or nothing.
Solution:
- Verify
IBeaconUserContextis registered (automatic withAddBeaconUI()) - Check that the claims transformer adds
BeaconClaims.UserName - Verify the user is authenticated
403 Forbidden on All Requests
Section titled “403 Forbidden on All Requests”Problem: All requests return 403 Forbidden.
Solution:
- Check the authorization provider is returning
truefor authenticated users - Add logging to the authorization provider:
_logger.LogWarning("Authorization denied for {User}: {Reason}",_userContext.UserName, result.FailureReason);
- Verify the claims transformer is executed
Provider Not Called
Section titled “Provider Not Called”Problem: Authorization provider methods never execute.
Solution:
- Verify the provider is registered:
options.AddAuthorizationProvider<MyAuthorizationProvider>();
- Check authorization is enabled
- Ensure
UseAuthorization()middleware is added
Security Best Practices
Section titled “Security Best Practices”- Always validate on the server - Client-side checks are for UX only
- Use HTTPS in production - Protect credentials and session cookies
- Store role assignments in the database - Don’t hardcode in the claims transformer
- Implement rate limiting - Prevent brute force attacks
- Audit authorization failures - Log all 403 responses with context
- Use strong session management - Implement proper timeout and renewal
- Validate resource ownership - Don’t rely on resource IDs alone
- Principle of least privilege - Default to the most restrictive permissions
Migration from Previous Versions
Section titled “Migration from Previous Versions”If you are upgrading from a version without authorization:
- No changes required - Authorization is disabled by default
- Opt-in when ready - Enable authorization when you’re ready
- No breaking changes - Existing code continues to work
- Gradual adoption - Start with global permissions, add resource-level later
See Also
Section titled “See Also”- User Management - Built-in user management with login form and role assignment
- Admin Settings - Runtime configuration (Admin-only)
- Configuration Guide
- Quick Start