In enterprise multi-tenant B2B SaaS platforms, authentication is significantly more complex than standard consumer apps:
- Hybrid Multi-Tenant Contexts: A single user may belong to multiple tenant organizations; the system must accurately resolve the active
TenantId; - Dual-Channel Ingestion: Supporting both browser-based interactive JWT authentication and external machine-to-machine (M2M) API Key integration;
- Context Tampering Defense: Callers must never be trusted to pass raw
UserIdorTenantIdparameters in request payloads.
BitzOrcas.Modern implements a unified authentication foundation: Extracting verified claims in ASP.NET Core middleware and injecting immutable, scoped ICurrentUser and ICurrentTenant contracts.
Authentication and Context Injection Pipeline
Step 1: Safely Accessing Identity from Scoped Contexts
Never read UserId from request bodies. Inject ICurrentUser and ICurrentTenant directly into handlers:
using System;using System.Threading;using System.Threading.Tasks;using BitzOrcas.Application.Abstractions.Security;using BitzOrcas.Domain.Abstractions;using BitzOrcas.Domain.Results;
public static class AuthErrors{ public static readonly Error Unauthenticated = Error.Unauthorized("Auth.Unauthenticated", "User must be authenticated.");}
public sealed class SubmitOrderCommandHandler( ICurrentUser currentUser, ICurrentTenant currentTenant, ICommandRepository<Order, string> orderRepository){ public async ValueTask<Result<string>> Handle(SubmitOrderCommand command, CancellationToken ct) { // 1. Extract verified UserId and TenantId from signed server context var userId = currentUser.UserId; var tenantId = currentTenant.Tenant.EffectiveTenantId;
// 2. Validate active session state if (string.IsNullOrEmpty(userId)) { return Result<string>.Failure(AuthErrors.Unauthenticated); }
// 3. Persist order with immutable server-resolved identities var order = Order.Create( id: Guid.NewGuid().ToString("N"), orderCode: command.OrderCode, totalAmount: command.TotalAmount, userId: userId, tenantId: tenantId).GetValueOrThrow();
var saveResult = await orderRepository.SaveAsync(order, ct); if (saveResult.IsFailure) { return Result<string>.Failure(saveResult.Error); }
return Result<string>.Success(order.Id); }}Step 2: Configuring API Key Machine Authentication
For external machine integrations, register high-throughput API Key validation handlers:
using Microsoft.AspNetCore.Authentication;using Microsoft.Extensions.Options;
public sealed class ApiKeyAuthenticationHandler( IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, System.Text.Encodings.Web.UrlEncoder encoder, IApiKeyValidator apiKeyValidator) : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder){ protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { // 1. Extract X-API-Key header if (!Request.Headers.TryGetValue("X-API-Key", out var apiKeyValue)) { return AuthenticateResult.NoResult(); }
// 2. Validate key against cached hashed store var validationResult = await apiKeyValidator.ValidateKeyAsync(apiKeyValue.ToString()); if (validationResult.IsFailure) { return AuthenticateResult.Fail("API Key is invalid or expired."); }
// 3. Construct ClaimsPrincipal representing machine identity var identity = validationResult.Value!; var ticket = new AuthenticationTicket(identity, Scheme.Name);
return AuthenticateResult.Success(ticket); }}Summary
BitzOrcas authentication unifies security across channels:
- Dual-Channel Parity: JWT and API Keys share identical authorization pipelines;
- Tamper-Proof Contexts: Identities are strictly generated by server verifications;
- Native Tenant Isolation: Auto-resolves effective tenant IDs, eliminating tenant leakage.