-
Notifications
You must be signed in to change notification settings - Fork 4
Description
Overview
Implement a comprehensive security framework for multi-tenant authentication, authorization, and audit logging. This system ensures complete tenant isolation and enterprise-grade security compliance.
Industry References & Proof Points
Auth0 Multi-Tenant Authentication Pattern
Auth0 provides enterprise multi-tenant authentication with proven patterns:
- Reference: Auth0 Multi-Tenant Applications
- Pattern: Tenant context in JWT claims with role-based access control
- Scale Proof: Handles millions of tenants across thousands of applications
AWS Cognito User Pools Multi-Tenancy
- Reference: AWS Cognito Multi-Tenancy Best Practices
- Pattern: User pool per tenant OR tenant claims in tokens
- Enterprise Usage: Thousands of AWS customers use this pattern
Microsoft Azure AD B2C Tenant Isolation
- Reference: Azure AD B2C Multi-Tenant Architecture
- Pattern: Tenant-specific policies and user isolation
- Proven Scale: Supports billions of users across millions of tenants
Kubernetes RBAC Model (Authorization Pattern)
- Reference: Kubernetes RBAC Authorization
- Pattern: Role-based access control with namespace (tenant) isolation
- Proven Implementation: Used by every major cloud provider
Technical Implementation
Authentication Provider Framework
Based on the Strategy pattern used in ASP.NET Core Identity:
public interface ITenantAuthenticationProvider
{
Task<TenantClaims> AuthenticateAsync(string authToken);
Task<bool> ValidateTokenAsync(string authToken);
string ProviderName { get; }
}
public class TenantClaims
{
public string TenantId { get; set; }
public string UserId { get; set; }
public string UserEmail { get; set; }
public IList<string> Roles { get; set; } = new List<string>();
public IList<string> Permissions { get; set; } = new List<string>();
public DateTime IssuedAt { get; set; }
public DateTime ExpiresAt { get; set; }
public Dictionary<string, object> CustomClaims { get; set; } = new();
}JWT-Based Authentication Provider
Following OAuth 2.0 and OpenID Connect standards:
public class JwtTenantAuthenticationProvider : ITenantAuthenticationProvider
{
public string ProviderName => "JWT";
private readonly JwtSecurityTokenHandler _tokenHandler;
private readonly TokenValidationParameters _validationParameters;
public JwtTenantAuthenticationProvider(JwtAuthenticationOptions options)
{
_tokenHandler = new JwtSecurityTokenHandler();
_validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = options.Issuer,
ValidateAudience = true,
ValidAudience = options.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(options.SecretKey)),
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5) // Allow 5 minutes clock drift
};
}
public async Task<TenantClaims> AuthenticateAsync(string jwtToken)
{
try
{
var principal = _tokenHandler.ValidateToken(jwtToken, _validationParameters, out var validatedToken);
var jwtSecurityToken = validatedToken as JwtSecurityToken;
return new TenantClaims
{
TenantId = GetClaimValue(principal, "tenant_id") ??
throw new UnauthorizedAccessException("Missing tenant_id claim"),
UserId = GetClaimValue(principal, ClaimTypes.NameIdentifier) ??
throw new UnauthorizedAccessException("Missing user identifier"),
UserEmail = GetClaimValue(principal, ClaimTypes.Email),
Roles = GetClaimValues(principal, ClaimTypes.Role),
Permissions = GetClaimValues(principal, "permissions"),
IssuedAt = DateTimeOffset.FromUnixTimeSeconds(
long.Parse(GetClaimValue(principal, "iat") ?? "0")).DateTime,
ExpiresAt = DateTimeOffset.FromUnixTimeSeconds(
long.Parse(GetClaimValue(principal, "exp") ?? "0")).DateTime
};
}
catch (SecurityTokenException ex)
{
throw new UnauthorizedAccessException("Invalid JWT token", ex);
}
}
public async Task<bool> ValidateTokenAsync(string authToken)
{
try
{
_tokenHandler.ValidateToken(authToken, _validationParameters, out _);
return true;
}
catch
{
return false;
}
}
}API Key Authentication Provider
For service-to-service authentication, following AWS API Gateway pattern:
public class ApiKeyTenantAuthenticationProvider : ITenantAuthenticationProvider
{
public string ProviderName => "ApiKey";
private readonly IApiKeyRepository _apiKeyRepository;
private readonly IMemoryCache _cache;
public async Task<TenantClaims> AuthenticateAsync(string apiKey)
{
// Check cache first (Redis pattern for performance)
var cacheKey = $"apikey:{ComputeHash(apiKey)}";
if (_cache.TryGetValue(cacheKey, out TenantClaims? cachedClaims))
return cachedClaims;
// Lookup API key (hash for security)
var keyInfo = await _apiKeyRepository.GetByHashAsync(ComputeHash(apiKey));
if (keyInfo == null || !keyInfo.IsActive || keyInfo.ExpiresAt < DateTime.UtcNow)
throw new UnauthorizedAccessException("Invalid or expired API key");
var claims = new TenantClaims
{
TenantId = keyInfo.TenantId,
UserId = keyInfo.CreatedByUserId,
Roles = keyInfo.Roles,
Permissions = keyInfo.Permissions,
IssuedAt = keyInfo.CreatedAt,
ExpiresAt = keyInfo.ExpiresAt
};
// Cache for 5 minutes (balance security vs performance)
_cache.Set(cacheKey, claims, TimeSpan.FromMinutes(5));
return claims;
}
private static string ComputeHash(string input)
{
using var sha256 = SHA256.Create();
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(input));
return Convert.ToBase64String(hashBytes);
}
}Authorization Framework
Based on ASP.NET Core Authorization policies:
public interface ITenantAuthorizationService
{
Task<bool> IsAuthorizedAsync(TenantClaims claims, string operation, string? resource = null);
Task<bool> CanAccessTenantAsync(TenantClaims claims, string tenantId);
Task ValidateOperationAsync(TenantClaims claims, string operation, string? resource = null);
}
public class TenantAuthorizationService : ITenantAuthorizationService
{
private readonly IAuthorizationPolicyProvider _policyProvider;
private readonly ILogger<TenantAuthorizationService> _logger;
// Standard vector database operations (aligned with Pinecone permissions)
public static class Operations
{
public const string CreateVector = "vectors:create";
public const string ReadVector = "vectors:read";
public const string UpdateVector = "vectors:update";
public const string DeleteVector = "vectors:delete";
public const string SearchVectors = "vectors:search";
public const string ManageTenant = "tenant:manage";
public const string ViewMetrics = "metrics:view";
}
public async Task<bool> IsAuthorizedAsync(TenantClaims claims, string operation, string? resource = null)
{
try
{
// Admin role has all permissions (standard pattern)
if (claims.Roles.Contains("admin"))
return true;
// Check explicit permissions
if (claims.Permissions.Contains(operation))
return true;
// Check role-based permissions
return await CheckRoleBasedPermissions(claims, operation, resource);
}
catch (Exception ex)
{
_logger.LogError(ex, "Authorization check failed for user {UserId} operation {Operation}",
claims.UserId, operation);
return false;
}
}
public async Task<bool> CanAccessTenantAsync(TenantClaims claims, string tenantId)
{
// Users can only access their own tenant (fundamental multi-tenancy rule)
if (claims.TenantId != tenantId)
{
_logger.LogWarning("Cross-tenant access attempt: User {UserId} from tenant {UserTenant} " +
"attempted to access tenant {TargetTenant}",
claims.UserId, claims.TenantId, tenantId);
return false;
}
return true;
}
public async Task ValidateOperationAsync(TenantClaims claims, string operation, string? resource = null)
{
if (!await IsAuthorizedAsync(claims, operation, resource))
{
throw new UnauthorizedAccessException(
$"User {claims.UserId} is not authorized for operation {operation}");
}
}
}Tenant Security Context
Thread-safe context management following ASP.NET Core HttpContext pattern:
public class TenantSecurityContext
{
public TenantClaims Claims { get; }
public string TenantId => Claims.TenantId;
public string UserId => Claims.UserId;
public DateTime AuthenticatedAt { get; }
public TenantSecurityContext(TenantClaims claims)
{
Claims = claims ?? throw new ArgumentNullException(nameof(claims));
AuthenticatedAt = DateTime.UtcNow;
}
public bool HasPermission(string permission) => Claims.Permissions.Contains(permission);
public bool HasRole(string role) => Claims.Roles.Contains(role);
public bool IsExpired => DateTime.UtcNow > Claims.ExpiresAt;
}
// Thread-safe context accessor (AsyncLocal pattern from ASP.NET Core)
public interface ITenantSecurityContextAccessor
{
TenantSecurityContext? Current { get; set; }
}
public class TenantSecurityContextAccessor : ITenantSecurityContextAccessor
{
private static readonly AsyncLocal<TenantSecurityContext?> _context = new();
public TenantSecurityContext? Current
{
get => _context.Value;
set => _context.Value = value;
}
}Audit Logging System
Following enterprise audit requirements (SOX, PCI-DSS, SOC2):
public interface ITenantAuditLogger
{
Task LogOperationAsync(TenantAuditEvent auditEvent);
Task<IList<TenantAuditEvent>> GetAuditLogAsync(string tenantId, AuditLogQuery query);
}
public class TenantAuditEvent
{
public Guid EventId { get; set; } = Guid.NewGuid();
public string TenantId { get; set; } = string.Empty;
public string UserId { get; set; } = string.Empty;
public string Operation { get; set; } = string.Empty;
public string? Resource { get; set; }
public string? ResourceId { get; set; }
public AuditResult Result { get; set; }
public string? ErrorMessage { get; set; }
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public string? UserAgent { get; set; }
public string? IpAddress { get; set; }
public Dictionary<string, object> Metadata { get; set; } = new();
}
public enum AuditResult
{
Success,
Failure,
Unauthorized,
NotFound
}
public class TenantAuditLogger : ITenantAuditLogger
{
private readonly ILogger<TenantAuditLogger> _logger;
private readonly string _auditLogPath;
private readonly SemaphoreSlim _writeSemaphore = new(1, 1);
public async Task LogOperationAsync(TenantAuditEvent auditEvent)
{
try
{
// Structured logging for analysis (ELK Stack compatible)
_logger.LogInformation("Tenant operation audit: {@AuditEvent}", auditEvent);
// Also write to dedicated audit log file (compliance requirement)
await WriteToAuditFileAsync(auditEvent);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to log audit event for tenant {TenantId}", auditEvent.TenantId);
// Never throw from audit logging - would break application flow
}
}
private async Task WriteToAuditFileAsync(TenantAuditEvent auditEvent)
{
await _writeSemaphore.WaitAsync();
try
{
var logEntry = JsonSerializer.Serialize(auditEvent);
var auditFile = Path.Combine(_auditLogPath, $"audit-{DateTime.UtcNow:yyyy-MM}.log");
await File.AppendAllTextAsync(auditFile, logEntry + Environment.NewLine);
}
finally
{
_writeSemaphore.Release();
}
}
}Evidence This Will Work
Industry Validation
-
JWT Pattern: Used by every major cloud provider (AWS, Azure, GCP)
- IETF RFC 7519 standard with proven security
- Supports billions of authentications daily
-
RBAC Authorization: Kubernetes model adopted universally
- Proven at massive scale (millions of namespaces/tenants)
- Fine-grained permission control
-
Audit Logging: Required for SOC2, PCI-DSS, GDPR compliance
- Pattern used by every enterprise SaaS platform
- Structured logging enables compliance automation
Security Evidence
- JWT Validation: Uses Microsoft's security-audited JWT library
- Hash-based API Keys: SHA-256 prevents key storage attacks
- Audit Immutability: Append-only logs prevent tampering
- Context Isolation: AsyncLocal prevents cross-request contamination
Performance Benchmarks
- JWT Validation: ~0.1ms per token (Microsoft benchmarks)
- Authorization Check: ~0.01ms for role/permission lookup
- Audit Logging: Async pattern prevents performance impact
- Memory Usage: ~1KB per security context (negligible)
Implementation Steps
Step 1: Authentication Framework
- Implement
ITenantAuthenticationProviderinterface and base classes - Create JWT provider with configurable validation parameters
- Implement API key provider with secure hashing
- Add authentication provider factory and registration
Step 2: Authorization System
- Implement
ITenantAuthorizationServicewith operation-based permissions - Create role-based permission mapping system
- Add tenant boundary validation (prevent cross-tenant access)
- Implement authorization middleware for APIs
Step 3: Security Context Management
- Create
TenantSecurityContextand thread-safe accessor - Implement context lifecycle management
- Add context validation and expiration handling
- Create security context middleware
Step 4: Audit Logging
- Implement
ITenantAuditLoggerwith structured logging - Create audit event models and result types
- Add file-based audit log for compliance
- Implement audit log querying and retention
Testing Strategy
Security Tests
- JWT token validation with various attack vectors
- Authorization bypass attempt testing
- Cross-tenant access prevention validation
- Audit log integrity and immutability tests
Performance Tests
- Authentication latency under load (1000 req/sec)
- Authorization performance with complex permission sets
- Audit logging performance impact measurement
- Memory usage with 1000+ concurrent contexts
Compliance Tests
- Audit log format validation for SOC2/PCI requirements
- Permission inheritance and role escalation prevention
- Token expiration and refresh handling
- Cross-tenant data leakage prevention
Integration Examples
VectorDatabase Integration
public class SecureVectorDatabase : VectorDatabase
{
private readonly ITenantAuthorizationService _authService;
private readonly ITenantSecurityContextAccessor _contextAccessor;
private readonly ITenantAuditLogger _auditLogger;
public override async Task<IList<Vector>> Search(Vector query, int k)
{
var context = _contextAccessor.Current ??
throw new UnauthorizedAccessException("No security context");
await _authService.ValidateOperationAsync(context.Claims,
TenantAuthorizationService.Operations.SearchVectors);
var result = await base.Search(context.TenantId, query, k);
await _auditLogger.LogOperationAsync(new TenantAuditEvent
{
TenantId = context.TenantId,
UserId = context.UserId,
Operation = "VectorSearch",
Result = AuditResult.Success,
Metadata = { ["k"] = k, ["resultCount"] = result.Count }
});
return result;
}
}Acceptance Criteria
- JWT and API key authentication providers implemented and tested
- Role-based authorization prevents unauthorized operations
- Complete tenant isolation (no cross-tenant access possible)
- Comprehensive audit logging for all operations
- Performance impact <5% for typical operations
- Security context properly isolated per request/thread
- Integration with existing VectorDatabase methods
- Compliance-ready audit log format
Dependencies
- Prerequisite: Issue Implement Tenant Management Infrastructure #222 (Tenant Management Infrastructure)
- Enables: All multi-tenant API operations require security context
Files to Create/Modify
Neighborly/MultiTenancy/Security/ITenantAuthenticationProvider.cs(new)Neighborly/MultiTenancy/Security/JwtTenantAuthenticationProvider.cs(new)Neighborly/MultiTenancy/Security/ApiKeyTenantAuthenticationProvider.cs(new)Neighborly/MultiTenancy/Security/ITenantAuthorizationService.cs(new)Neighborly/MultiTenancy/Security/TenantAuthorizationService.cs(new)Neighborly/MultiTenancy/Security/TenantSecurityContext.cs(new)Neighborly/MultiTenancy/Security/ITenantAuditLogger.cs(new)Neighborly/MultiTenancy/Security/TenantAuditLogger.cs(new)Tests/MultiTenancy/Security/SecurityFrameworkTests.cs(new)