From ebda614aaa662b82d631ccba7b2f3d966fa6dd24 Mon Sep 17 00:00:00 2001 From: kawa Date: Tue, 9 Jun 2026 14:34:58 +0200 Subject: [PATCH] Fix prod auth: persist DataProtection keys; redirect unauth to login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two deployment-breaking issues caused 404s on protected pages after a container recreate: 1. DataProtection keys were stored in the container's ephemeral home dir. Every redeploy regenerated them, invalidating all auth cookies (users silently logged out) and — worse — making the app-only certs encrypted under /data/appcerts undecryptable. Persist keys to /data/dpkeys with a stable application name so they survive recreates. 2. DefaultChallengeScheme was OpenIdConnect, so a logged-out request to any [Authorize] Blazor page forced an OIDC challenge. When OIDC is unconfigured/unreachable the challenge throws and the request 404s, with no path to the login page. Challenge the cookie scheme instead, which redirects to /account/login (the combined local + Microsoft page). OIDC is still triggered explicitly from /account/login/entra. Also harden the container image: - Pin base images to exact patch (sdk:10.0.300, aspnet:10.0.8). Floating :10.0 tags drift; a stale/pre-GA SDK base silently drops blazor.web.js from the publish manifest, 404ing framework assets in production. - Install curl and switch the compose healthcheck to it (the aspnet image ships no wget/curl, so the old healthcheck always reported unhealthy). Probe /account/login (anonymous, 200) since / now 302-redirects. Co-Authored-By: Claude Opus 4.8 (1M context) --- Dockerfile | 11 +++++++++-- Program.cs | 18 ++++++++++++++++-- docker-compose.yml | 5 ++++- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index f58ae1c..296618d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,15 @@ -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +# Base images pinned to exact patch for reproducible builds. Floating `:10.0` tags +# drift; a stale/pre-GA SDK base silently drops the Blazor framework static assets +# (blazor.web.js) from the publish manifest → 404 in production. Bump deliberately. +FROM mcr.microsoft.com/dotnet/aspnet:10.0.8 AS base WORKDIR /app EXPOSE 8080 +# curl for the compose healthcheck (aspnet image ships no wget/curl). +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl \ + && rm -rf /var/lib/apt/lists/* -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0.300 AS build WORKDIR /src COPY ["SharepointToolbox.Web.csproj", "."] RUN dotnet restore diff --git a/Program.cs b/Program.cs index 9019fb8..5b45224 100644 --- a/Program.cs +++ b/Program.cs @@ -43,6 +43,15 @@ builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); builder.Services.AddHttpContextAccessor(); +// ── Data Protection ─────────────────────────────────────────────────────────── +// Keys MUST persist across container recreates: they encrypt the auth cookie AND +// the app-only certs on disk (/data/appcerts). Default storage is the container's +// ephemeral home dir, so a redeploy would log everyone out and make stored certs +// undecryptable. Pin keys + app name to the data volume. +builder.Services.AddDataProtection() + .PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(dataFolder, "dpkeys"))) + .SetApplicationName("SharepointToolbox.Web"); + // Localization string source — Scoped: one per circuit, with its own explicit culture. builder.Services.AddScoped(); @@ -65,8 +74,13 @@ else { builder.Services.AddAuthentication(options => { - options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + // Challenge the cookie scheme (→ redirect to /account/login, the combined + // local + Microsoft page). OIDC is triggered explicitly from the "Sign in + // with Microsoft" button (/account/login/entra), never as the implicit + // challenge — otherwise logged-out hits on protected pages force OIDC and + // 404 when it is unconfigured/unreachable. + options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; }) .AddCookie(options => { diff --git a/docker-compose.yml b/docker-compose.yml index 1fe0b40..6b69790 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,10 @@ services: - DataFolder=/data restart: unless-stopped healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/"] + # /account/login is anonymous and returns 200 (the app root now 302-redirects + # unauthenticated users, which would read as unhealthy). curl is installed in + # the image; -f fails on >=400. + test: ["CMD", "curl", "-fsS", "http://localhost:8080/account/login"] interval: 30s timeout: 10s retries: 3