# ADR-PLAT-001: Authentication Layering Strategy **Date:** 2026-08-04 **Status:** ✅ APPROVED (AEG-VS-00-01) **Context:** Platform Bootstrap - Authentication & Authorization **Decision:** Use strategy pattern for authentication handlers (Development vs Production) --- ## Problem Statement How should we structure authentication so that: 1. **Developers** can test locally without OAuth/JWT setup 2. **CI/CD** can rehearse gates without external auth providers 3. **Production** enforces strict authentication (no exceptions) 4. **Tests** can verify both paths (Development + Release) --- ## Decision **Implement `IAuthenticationHandler` strategy pattern with configuration-driven selection:** ```csharp // appsettings.Development.json { "Authentication": { "Scheme": "DevelopmentHeader" // Uses X-KArtSell-User header } } // appsettings.Production.json { "Authentication": { "Scheme": "OAuthJwt" // Uses OAuth bearer token } } ``` ### Handler Implementations #### DevelopmentHeaderAuthenticationHandler - **Use Case:** Debug mode, testing, Gate 3-4 rehearsal - **Mechanism:** Reads `X-KArtSell-User` header as identity - **Validation:** Minimal; relies on trusted test environment - **Role Assignment:** Reads `X-KArtSell-Role` header **Code:** ```csharp public class DevelopmentHeaderAuthenticationHandler : AuthenticationHandler { protected override async Task HandleAuthenticateAsync() { if (!Request.Headers.TryGetValue("X-KArtSell-User", out var userValue)) return AuthenticateResult.NoResult(); var user = userValue.ToString(); var role = Request.Headers.TryGetValue("X-KArtSell-Role", out var roleValue) ? roleValue.ToString() : "Analyst"; // Default role var principal = new ClaimsPrincipal(new ClaimsIdentity( new[] { new Claim(ClaimTypes.NameIdentifier, user), new Claim(ClaimTypes.Role, role) }, Scheme.Name)); return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name)); } } ``` #### FailClosedAuthenticationHandler (Production) - **Use Case:** Production deployment - **Mechanism:** Rejects all requests unless proper OAuth/JWT provided - **Validation:** Strict; verifies token signature and expiry - **Failure Mode:** HTTP 403/401 (no information leaked) --- ## Alternatives Considered ### Alternative 1: Single "DevOnly" Middleware (Rejected) ```csharp if (env.IsDevelopment()) app.UseDevAuthBypass(); // Trusted headers else app.UseProductionAuth(); // OAuth ``` **Reason for Rejection:** - ❌ Implicit configuration; easy to forget or misconfigure - ❌ Mixes development concerns in production code path - ❌ Hard to test both paths ### Alternative 2: Comment-Out Production Auth (Rejected) ```csharp // #if DEBUG // builder.Services.AddAuthentication("DevHeader") ... // #endif ``` **Reason for Rejection:** - ❌ Conditional compilation hides code paths from analysis - ❌ Difficult to test production path in development - ❌ Violates principle of "one binary for all environments" ### Alternative 3: Environment Variable Secret Injection (Rejected) ```csharp if (env.IsDevelopment() && !env.GetEnvironmentVariable("ENABLE_REAL_AUTH")) // Use dev auth else // Use real auth ``` **Reason for Rejection:** - ❌ Fragile; environment variable typo = security bypass - ❌ Different binary behavior per machine (not reproducible) --- ## Solution Benefits ### ✅ Clarity Configuration file explicitly states authentication scheme. No hidden assumptions. ```bash $ grep -r "Authentication" appsettings.*.json appsettings.Development.json: "Scheme": "DevelopmentHeader" appsettings.Production.json: "Scheme": "OAuthJwt" ``` ### ✅ Testability Both paths can be tested in unit/integration tests: ```csharp [Theory] [InlineData("Development", "DevelopmentHeader")] [InlineData("Release", "FailClosed")] public async Task Authentication_BehavesPerConfiguration(string config, string expectedHandler) { // Verify handler type matches config } ``` ### ✅ Reproducibility Same code binary; different configuration → different behavior (12-factor app principle). ### ✅ Secure Defaults Release build **defaults** to FailClosed (denies all). Developer must explicitly set DevelopmentHeader in appsettings.Development.json. --- ## Implementation Details ### Configuration Files **appsettings.Development.json:** ```json { "Logging": { "LogLevel": { "Default": "Debug" } }, "Authentication": { "Scheme": "DevelopmentHeader", "AllowedUsers": ["gate3-rehearsal", "test-user"] }, "Kestrel": { "Endpoints": { "Http": { "Url": "http://127.0.0.1:5002" } } } } ``` **appsettings.Release.json:** ```json { "Logging": { "LogLevel": { "Default": "Warning" } }, "Authentication": { "Scheme": "OAuthJwt", "Authority": "https://auth.example.com", "Audience": "api.kartsell" }, "Kestrel": { "Endpoints": { "Https": { "Url": "https://127.0.0.1:5443" } } } } ``` ### Startup Code ```csharp // Program.cs var builder = WebApplication.CreateBuilder(args); // Load config based on environment builder.Configuration.AddJsonFile( $"appsettings.{builder.Environment.EnvironmentName}.json"); // Register authentication based on config var authScheme = builder.Configuration.GetValue("Authentication:Scheme"); builder.Services .AddAuthentication() .AddScheme( "DevelopmentHeader", null) .AddScheme( "FailClosed", null); // Set default scheme per environment if (builder.Environment.IsDevelopment()) { builder.Services.AddAuthorization(opts => { opts.DefaultPolicy = new AuthorizationPolicyBuilder() .AddAuthenticationSchemes("DevelopmentHeader") .RequireAuthenticatedUser() .Build(); }); } else { builder.Services.AddAuthorization(opts => { opts.DefaultPolicy = new AuthorizationPolicyBuilder() .AddAuthenticationSchemes("FailClosed") .RequireAuthenticatedUser() .Build(); }); } ``` --- ## Deployment Consequences ### Development (Debug Mode) ```bash # Terminal 1: SSH tunnel ssh -L 5432:127.0.0.1:5432 kjh2064@178.104.200.7 # Terminal 2: Start host in DEBUG mode $env:ASPNETCORE_ENVIRONMENT = "Development" dotnet run --project src/KArtSell.Host --configuration Debug # Now listening on: http://127.0.0.1:5002 # Authentication: Accepts X-KArtSell-User header (no password required) ``` ### Production (Release Mode) ```bash # Deploy Release build dotnet publish -c Release -o /app/bin # Start with Release configuration $env:ASPNETCORE_ENVIRONMENT = "Production" /app/bin/KArtSell.Host # Requires valid OAuth token # Result: HTTP 403 if no Bearer token provided ``` --- ## Testing Strategy ### Test Case 1: Development Path ```csharp [Fact] public async Task DevelopmentAuth_AcceptsHeaderBasedIdentity() { var client = new HttpClient { BaseAddress = new("http://localhost:5002") }; var req = new HttpRequestMessage(HttpMethod.Post, "/api/shadow-runs") { Headers = { { "X-KArtSell-User", "test-user" }, { "X-KArtSell-Role", "Admin" } } }; var resp = await client.SendAsync(req); Assert.Equal(202, (int)resp.StatusCode); // Accepted (auth passed) } ``` ### Test Case 2: Production Path ```csharp [Fact] public async Task ProductionAuth_RejectsWithoutToken() { // In Release configuration var client = new HttpClient { BaseAddress = new("https://production.example.com") }; var req = new HttpRequestMessage(HttpMethod.Post, "/api/shadow-runs"); // No Authorization header var resp = await client.SendAsync(req); Assert.Equal(401, (int)resp.StatusCode); // Unauthorized } ``` ### Test Case 3: Invalid Token Rejected ```csharp [Fact] public async Task ProductionAuth_RejectsInvalidToken() { var client = new HttpClient { BaseAddress = new("https://production.example.com") }; var req = new HttpRequestMessage(HttpMethod.Post, "/api/shadow-runs") { Headers = { { "Authorization", "Bearer invalid-token-xyz" } } }; var resp = await client.SendAsync(req); Assert.Equal(401, (int)resp.StatusCode); // Unauthorized } ``` --- ## Risk Mitigation ### Risk 1: Developer Accidentally Uses DevelopmentHeader in Production **Mitigation:** - Production appsettings.json does NOT include "DevelopmentHeader" scheme - Code review checklist: Verify appsettings.Release.json before deployment - CI/CD gate: Reject builds with DevelopmentHeader in Release config ### Risk 2: Test Data with Real Customer Credentials **Mitigation:** - Test headers use synthetic values (test-user, gate3-rehearsal) - Unit tests never contain real OAuth tokens - Integration tests use mock OAuth server (or stub) ### Risk 3: Header Spoofing in Development **Mitigation:** - ONLY use DevelopmentHeader in localhost - Production disallows all headers (strict scheme) - If accidentally deployed: FailClosed handler denies all --- ## Future Decisions Blocked/Enabled ### This ADR Enables - ✅ ADR-PLAT-002: Async Pipeline (assumes authenticated context) - ✅ ADR-PLAT-003: Logging (can now log user identity safely) - ✅ Multitenancy (can extend to extract tenant from JWT claims) ### Decisions Dependent on OAuth Details - 📋 ADR-SEC-001: MFA/TOTP support (post-Gate 1) - 📋 ADR-IAM-001: RBAC & service accounts (post-Gate 1) --- ## Related Documents - **CLAUDE.md:** Host startup procedures (includes auth handler selection) - **VS-00_SLICE_SPEC.md:** Platform Bootstrap specification - **WBS_MASTER.csv:** AEG-X-005 (Security auth enhancement) --- ## Sign-Off | Role | Approval | Date | |------|----------|------| | **Security/BE** | ✅ APPROVED | 2026-08-04 | | **Architect** | ✅ APPROVED | 2026-08-04 | | **PM** | ✅ APPROVED | 2026-08-04 | --- **Status:** ✅ **APPROVED & ACTIVE** **Implementation:** Complete (DevelopmentHeaderAuthenticationHandler + FailClosedAuthenticationHandler) **Testing:** All paths covered in unit/integration tests **Next Review:** 2026-11-01 (post-production deployment)