From ef8a0e9c6ed3562ce4a00fff2fc25b96cf056e6f Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Mon, 25 May 2026 11:26:29 -0400 Subject: [PATCH] Move AuthBlocks API host (registration, migration/seed, endpoint mounting) from Manager to DeepDrftAPI; Manager keeps only AuthBlocksWeb --- DeepDrftAPI/DeepDrftAPI.csproj | 6 +- DeepDrftAPI/Program.cs | 58 +++++++++++++++ DeepDrftAPI/appsettings.json | 2 + DeepDrftAPI/authblocks.example.json | 19 +++++ DeepDrftAPI/connections.example.json | 6 ++ DeepDrftManager/DeepDrftManager.csproj | 1 - DeepDrftManager/Program.cs | 99 ++++---------------------- 7 files changed, 102 insertions(+), 89 deletions(-) create mode 100644 DeepDrftAPI/authblocks.example.json create mode 100644 DeepDrftAPI/connections.example.json diff --git a/DeepDrftAPI/DeepDrftAPI.csproj b/DeepDrftAPI/DeepDrftAPI.csproj index a708f28..d05cdda 100644 --- a/DeepDrftAPI/DeepDrftAPI.csproj +++ b/DeepDrftAPI/DeepDrftAPI.csproj @@ -12,6 +12,9 @@ + + @@ -20,8 +23,5 @@ - - - diff --git a/DeepDrftAPI/Program.cs b/DeepDrftAPI/Program.cs index ce3dbb9..a8da24e 100644 --- a/DeepDrftAPI/Program.cs +++ b/DeepDrftAPI/Program.cs @@ -1,3 +1,5 @@ +using AuthBlocksLib; +using AuthBlocksLib.Options; using DeepDrftAPI; using DeepDrftAPI.Middleware; using DeepDrftAPI.Models; @@ -9,6 +11,12 @@ using Microsoft.AspNetCore.HttpOverrides; using Microsoft.EntityFrameworkCore; using NetBlocks.Utilities.Environment; +// Required credential files — must exist before the app will start. +// Production secrets stay gitignored; the *.example.json templates at the project root show the shape. +// - environment/filedatabase.json: { "FileDatabaseSettings": { "VaultPath": "..." } } +// - environment/apikey.json: { "ApiKeySettings": { "ApiKey": "..." } } +// - environment/connections.json: { "ConnectionStrings": { "DefaultConnection": "...", "Auth": "..." } } +// - environment/authblocks.json: { "AuthBlocks": { "Jwt": {...}, "Email": {...}, "Admin": {...} } } var builder = WebApplication.CreateBuilder(args); // Add services to the container. @@ -56,6 +64,43 @@ builder.Services .AddScoped(sp => sp.GetRequiredService()); builder.Services.AddScoped(); +// AuthBlocks: JWT Bearer auth, Identity, EF schema, role + admin seeding. This API host owns the +// AuthBlocks API surface (registration, migration/seed, endpoint mounting). The Manager keeps only +// web-side auth (AuthBlocksWeb) and never holds the signing secret, email creds, or admin creds. +// Auth schema runs in its own database (separate from DefaultConnection by design). +var authBlocksPath = CredentialTools.ResolvePathOrThrow("authblocks", "environment/authblocks.json"); +builder.Configuration.AddJsonFile(authBlocksPath, optional: false, reloadOnChange: false); + +builder.Services.AddAuthBlocks(options => +{ + options.ConnectionString = builder.Configuration.GetConnectionString("Auth") + ?? throw new InvalidOperationException("ConnectionStrings:Auth is required"); + options.ApplicationName = "DeepDrft"; + options.SupportEmail = builder.Configuration["AuthBlocks:SupportEmail"] ?? "admin@deepdrft.com"; + + options.JwtSettings.Secret = builder.Configuration["AuthBlocks:Jwt:Secret"] + ?? throw new InvalidOperationException("AuthBlocks:Jwt:Secret is required"); + options.JwtSettings.Issuer = builder.Configuration["AuthBlocks:Jwt:Issuer"] + ?? throw new InvalidOperationException("AuthBlocks:Jwt:Issuer is required"); + options.JwtSettings.Audience = builder.Configuration["AuthBlocks:Jwt:Audience"] + ?? throw new InvalidOperationException("AuthBlocks:Jwt:Audience is required"); + + options.EmailConnection.Host = builder.Configuration["AuthBlocks:Email:Host"] + ?? throw new InvalidOperationException("AuthBlocks:Email:Host is required"); + options.EmailConnection.Token = builder.Configuration["AuthBlocks:Email:Token"] + ?? throw new InvalidOperationException("AuthBlocks:Email:Token is required"); + + options.AdminUserSettings = new AdminUserSettings + { + UserName = builder.Configuration["AuthBlocks:Admin:UserName"] + ?? throw new InvalidOperationException("AuthBlocks:Admin:UserName is required"), + Email = builder.Configuration["AuthBlocks:Admin:Email"] + ?? throw new InvalidOperationException("AuthBlocks:Admin:Email is required"), + Password = builder.Configuration["AuthBlocks:Admin:Password"] + ?? throw new InvalidOperationException("AuthBlocks:Admin:Password is required") + }; +}); + // Configure forwarded headers for reverse proxy support builder.Services.Configure(options => { @@ -67,6 +112,9 @@ builder.Services.Configure(options => var app = builder.Build(); +// Apply AuthBlocks EF migrations, seed system roles, seed admin user on first boot. +await app.Services.UseAuthBlocksStartupAsync(); + if (app.Environment.IsProduction()) { // Use forwarded headers before other middleware @@ -79,8 +127,18 @@ if (app.Environment.IsDevelopment()) } app.UseCors("ContentApiPolicy"); + +// ApiKey middleware only enforces on endpoints tagged [ApiKeyAuthorize] (the track surface); it +// passes all other endpoints through. JWT auth/authorization gate the AuthBlocks endpoints, which +// carry no [ApiKeyAuthorize] metadata — the two schemes are orthogonal and do not interfere. app.UseApiKeyAuthentication(apiKeySettings.ApiKey); +app.UseAuthentication(); +app.UseAuthorization(); app.MapControllers(); +// Mount the AuthBlocks API surface (/api/auth/*, /api/users/*, /api/roles/*, /api/user-roles/*, +// /api/pending-registrations/*). Protected routes require the JWT bearer scheme registered above. +app.MapAuthBlocks(); + app.Run(); \ No newline at end of file diff --git a/DeepDrftAPI/appsettings.json b/DeepDrftAPI/appsettings.json index 4046d61..08240ca 100644 --- a/DeepDrftAPI/appsettings.json +++ b/DeepDrftAPI/appsettings.json @@ -10,6 +10,8 @@ "CorsSettings": { "AllowedOrigins": [ "https://localhost:12778", + "https://localhost:5004", + "http://localhost:5003", "https://deepdrft.com", "https://www.deepdrft.com" ] diff --git a/DeepDrftAPI/authblocks.example.json b/DeepDrftAPI/authblocks.example.json new file mode 100644 index 0000000..e7540f1 --- /dev/null +++ b/DeepDrftAPI/authblocks.example.json @@ -0,0 +1,19 @@ +{ + "AuthBlocks": { + "SupportEmail": "admin@deepdrft.com", + "Jwt": { + "Secret": "your-jwt-secret-here-min-32-chars", + "Issuer": "https://deepdrft.com", + "Audience": "deepdrft-users" + }, + "Email": { + "Host": "smtp.your-provider.com", + "Token": "your-email-token-here" + }, + "Admin": { + "UserName": "admin", + "Email": "admin@deepdrft.com", + "Password": "your-admin-password-here" + } + } +} diff --git a/DeepDrftAPI/connections.example.json b/DeepDrftAPI/connections.example.json new file mode 100644 index 0000000..d411981 --- /dev/null +++ b/DeepDrftAPI/connections.example.json @@ -0,0 +1,6 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5433;Database=deepdrft;Username=postgres;Password=your-password-here", + "Auth": "Host=localhost;Port=5433;Database=deepdrft_auth;Username=postgres;Password=your-password-here" + } +} diff --git a/DeepDrftManager/DeepDrftManager.csproj b/DeepDrftManager/DeepDrftManager.csproj index 02d1ab7..f7315d8 100644 --- a/DeepDrftManager/DeepDrftManager.csproj +++ b/DeepDrftManager/DeepDrftManager.csproj @@ -8,7 +8,6 @@ - diff --git a/DeepDrftManager/Program.cs b/DeepDrftManager/Program.cs index 7ed5c8f..2b68a0a 100644 --- a/DeepDrftManager/Program.cs +++ b/DeepDrftManager/Program.cs @@ -1,5 +1,3 @@ -using AuthBlocksLib; -using AuthBlocksLib.Options; using DeepDrftManager.Components; using DeepDrftManager.Services; using Microsoft.AspNetCore.HttpOverrides; @@ -11,18 +9,13 @@ var builder = WebApplication.CreateBuilder(args); // Required credential files — must exist before the app will start. // Production secrets stay gitignored; the *.example.json templates at the project root show the shape. // - environment/api.json: { "Api": { "ContentApiUrl": "...", "ContentApiKey": "..." } } -// - environment/connections.json: { "ConnectionStrings": { "DefaultConnection": "...", "Auth": "..." } } -// - environment/authblocks.json: { "AuthBlocks": { "Jwt": {...}, "Email": {...}, "Admin": {...} } } +// The Manager hosts only web-side auth (AuthBlocksWeb), which talks to the AuthBlocks API on +// DeepDrftAPI. It holds no JWT signing secret, email creds, admin creds, or Auth connection string — +// those moved to DeepDrftAPI's environment/authblocks.json + environment/connections.json. // Content API key — consumed by CmsTrackService for the upload proxy and the vault-delete client. var apiPath = CredentialTools.ResolvePathOrThrow("api", "environment/api.json"); builder.Configuration.AddJsonFile(apiPath, optional: false, reloadOnChange: false); -var connectionsPath = CredentialTools.ResolvePathOrThrow("connections", "environment/connections.json"); -builder.Configuration.AddJsonFile(connectionsPath, optional: false, reloadOnChange: false); - -var authBlocksPath = CredentialTools.ResolvePathOrThrow("authblocks", "environment/authblocks.json"); -builder.Configuration.AddJsonFile(authBlocksPath, optional: false, reloadOnChange: false); - // MudBlazor. builder.Services.AddMudServices(); @@ -30,47 +23,15 @@ builder.Services.AddMudServices(); // DeepDrftAPI API via the named clients below — the Manager holds no in-process data layer. builder.Services.AddScoped(); -// AuthBlocks: JWT Bearer auth, Identity, EF schema, admin seeding. -// Auth schema runs in its own database (separate from DefaultConnection by design). -builder.Services.AddAuthBlocks(options => -{ - options.ConnectionString = builder.Configuration.GetConnectionString("Auth") - ?? throw new InvalidOperationException("ConnectionStrings:Auth is required"); - options.ApplicationName = "DeepDrft"; - options.SupportEmail = builder.Configuration["AuthBlocks:SupportEmail"] ?? "admin@deepdrft.com"; - - options.JwtSettings.Secret = builder.Configuration["AuthBlocks:Jwt:Secret"] - ?? throw new InvalidOperationException("AuthBlocks:Jwt:Secret is required"); - options.JwtSettings.Issuer = builder.Configuration["AuthBlocks:Jwt:Issuer"] - ?? throw new InvalidOperationException("AuthBlocks:Jwt:Issuer is required"); - options.JwtSettings.Audience = builder.Configuration["AuthBlocks:Jwt:Audience"] - ?? throw new InvalidOperationException("AuthBlocks:Jwt:Audience is required"); - - options.EmailConnection.Host = builder.Configuration["AuthBlocks:Email:Host"] - ?? throw new InvalidOperationException("AuthBlocks:Email:Host is required"); - options.EmailConnection.Token = builder.Configuration["AuthBlocks:Email:Token"] - ?? throw new InvalidOperationException("AuthBlocks:Email:Token is required"); - - options.AdminUserSettings = new AdminUserSettings - { - UserName = builder.Configuration["AuthBlocks:Admin:UserName"] - ?? throw new InvalidOperationException("AuthBlocks:Admin:UserName is required"), - Email = builder.Configuration["AuthBlocks:Admin:Email"] - ?? throw new InvalidOperationException("AuthBlocks:Admin:Email is required"), - Password = builder.Configuration["AuthBlocks:Admin:Password"] - ?? throw new InvalidOperationException("AuthBlocks:Admin:Password is required") - }; -}); - // AuthBlocksWeb: server-side cascading auth state plus the JWT client services used by the // /account/login + /account/logout Razor pages that ship in the AuthBlocksWeb RCL. -var baseUrl = GetKestrelUrl(builder); -AuthBlocksWeb.Startup.ConfigureAuthServices(builder.Services, baseUrl); +// The auth API lives on DeepDrftAPI, so pass its URL — not Manager's own Kestrel URL. +var contentApiUrl = builder.Configuration["Api:ContentApiUrl"] + ?? throw new InvalidOperationException("Api:ContentApiUrl is required"); +AuthBlocksWeb.Startup.ConfigureAuthServices(builder.Services, contentApiUrl); // Named HttpClient for unauthenticated Content API calls (CmsTrackService proxying WAV data // to DeepDrftAPI's POST api/track/upload). API key added per-request by the service. -var contentApiUrl = builder.Configuration["Api:ContentApiUrl"] - ?? throw new InvalidOperationException("Api:ContentApiUrl is required"); builder.Services.AddHttpClient("DeepDrft.Content", client => { client.BaseAddress = new Uri(contentApiUrl); @@ -115,9 +76,6 @@ builder.Services.AddSignalR(options => var app = builder.Build(); -// Apply AuthBlocks EF migrations, seed system roles, seed admin user on first boot. -await app.Services.UseAuthBlocksStartupAsync(); - app.UseForwardedHeaders(); if (!app.Environment.IsDevelopment()) @@ -138,45 +96,16 @@ app.UseAuthorization(); app.MapStaticAssets(); -// Mount AuthBlocks API surface (/api/auth/*, /api/users/*, etc.) and the AuthBlocksWeb -// Razor pages (/account/login, /account/logout). -app.MapAuthBlocks(); - -// Blazor page authorization is owned by AuthorizeRouteView in Routes.razor, not -// ASP.NET Core endpoint authorization. AuthBlocks tokens live in browser localStorage -// (read via JS interop by JwtAuthenticationStateProvider), so the JWT never reaches -// the server on a navigation request. Without AllowAnonymous here, the JwtBearer -// challenge for an unauthenticated nav returns 401 before the Blazor router runs, -// short-circuiting the NotAuthorized -> RedirectToLogin path. JWT enforcement -// remains in force for the AuthBlocks API surface (MapAuthBlocks). +// The AuthBlocks API surface (/api/auth/*, /api/users/*, etc.) now lives on DeepDrftAPI; this host +// only renders the AuthBlocksWeb Razor pages (/account/login, /account/logout), which call that API. +// Blazor page authorization is owned by AuthorizeRouteView in Routes.razor, not ASP.NET Core +// endpoint authorization. AuthBlocks tokens live in browser localStorage (read via JS interop by +// JwtAuthenticationStateProvider), so the JWT never reaches the server on a navigation request. +// Without AllowAnonymous here, the cookie/JwtBearer challenge for an unauthenticated nav returns 401 +// before the Blazor router runs, short-circuiting the NotAuthorized -> RedirectToLogin path. app.MapRazorComponents() .AddInteractiveServerRenderMode() .AddAdditionalAssemblies(typeof(AuthBlocksWeb._Imports).Assembly) .AllowAnonymous(); app.Run(); - -// Local helper — mirrors DeepDrftPublic.Startup.GetKestrelUrl. Kept inline because this host's -// only consumer is right here; promoting to a shared library would be premature. -static string GetKestrelUrl(WebApplicationBuilder builder) -{ - var urls = builder.Configuration["ASPNETCORE_URLS"] - ?? builder.Configuration["urls"]; - - if (!string.IsNullOrEmpty(urls)) - { - return urls.Split(';')[0].Trim(); - } - - var firstEndpoint = builder.Configuration.GetSection("Kestrel:Endpoints").GetChildren().FirstOrDefault(); - var endpointUrl = firstEndpoint?["Url"]; - - if (!string.IsNullOrEmpty(endpointUrl)) - { - return endpointUrl; - } - - return builder.Environment.IsDevelopment() - ? "https://localhost:5004" - : "http://localhost:5000"; -}