Fix stuck-on-loading after sign-in; enable HTTP/LAN local login #3
@@ -1,6 +1,7 @@
|
||||
@inject AuthenticationStateProvider AuthProvider
|
||||
@inject IUserService UserService
|
||||
@inject IUserContextAccessor UserContext
|
||||
@inject ILogger<AppInitializer> Logger
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using SharepointToolbox.Web.Services.Auth
|
||||
@using SharepointToolbox.Web.Services.Session
|
||||
@@ -12,13 +13,30 @@
|
||||
{
|
||||
var state = await AuthProvider.GetAuthenticationStateAsync();
|
||||
var principal = state.User;
|
||||
if (principal.Identity?.IsAuthenticated != true) return;
|
||||
|
||||
if (principal.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
Logger.LogWarning("AppInitializer: circuit principal NOT authenticated; UserContext left unseeded → page stays on loading.");
|
||||
return;
|
||||
}
|
||||
|
||||
var email = principal.FindFirst("preferred_username")?.Value
|
||||
?? principal.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value;
|
||||
if (string.IsNullOrEmpty(email)) return;
|
||||
if (string.IsNullOrEmpty(email))
|
||||
{
|
||||
var claims = string.Join(", ", principal.Claims.Select(c => $"{c.Type}={c.Value}"));
|
||||
Logger.LogWarning("AppInitializer: authenticated but no preferred_username/email claim. Claims present: [{Claims}]", claims);
|
||||
return;
|
||||
}
|
||||
|
||||
var user = await UserService.GetByEmailAsync(email);
|
||||
if (user is not null) UserContext.Initialize(user);
|
||||
if (user is null)
|
||||
{
|
||||
Logger.LogWarning("AppInitializer: no user row for email '{Email}' — provisioning did not persist a matching record.", email);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInformation("AppInitializer: seeded UserContext for '{Email}' (role {Role}).", user.Email, user.Role);
|
||||
UserContext.Initialize(user);
|
||||
}
|
||||
}
|
||||
|
||||
+49
-1
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||
using Microsoft.AspNetCore.Antiforgery;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||
@@ -43,6 +44,19 @@ builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
// ── Forwarded headers ─────────────────────────────────────────────────────────
|
||||
// Behind a TLS-terminating reverse proxy the app receives plain HTTP; without this
|
||||
// it would see scheme=http and build an http:// OIDC redirect_uri (which Entra
|
||||
// rejects for non-localhost hosts) and set the auth cookie non-Secure. Honour
|
||||
// X-Forwarded-Proto/For so the app sees the real https scheme + client IP. Proxy IP
|
||||
// is unknown inside the container network, so don't restrict to known proxies.
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||
options.KnownIPNetworks.Clear();
|
||||
options.KnownProxies.Clear();
|
||||
});
|
||||
|
||||
// ── 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
|
||||
@@ -100,7 +114,13 @@ else
|
||||
options.ClientId = oidc["ClientId"];
|
||||
options.ClientSecret = oidc["ClientSecret"];
|
||||
options.ResponseType = OpenIdConnectResponseType.Code;
|
||||
options.SaveTokens = true;
|
||||
// Do NOT persist the OIDC access/id/refresh tokens in the auth cookie. They are
|
||||
// never read (SharePoint/Graph auth runs through the separate connect flow +
|
||||
// app-only cert paths), and storing them bloats the cookie past ~4 KB so it gets
|
||||
// chunked. The chunked cookie survives the prerender GET but is dropped on the
|
||||
// WebSocket upgrade that establishes the interactive circuit → the circuit comes
|
||||
// up anonymous and the app sticks on "Chargement…". Keeping the cookie small fixes it.
|
||||
options.SaveTokens = false;
|
||||
options.Scope.Add("openid");
|
||||
options.Scope.Add("profile");
|
||||
options.Scope.Add("email");
|
||||
@@ -212,6 +232,34 @@ builder.Services.AddHostedService<ScheduledReportHostedService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Must run before anything that inspects the request scheme/IP (auth, OIDC, cookies).
|
||||
app.UseForwardedHeaders();
|
||||
|
||||
// ── First-run bootstrap ───────────────────────────────────────────────────────
|
||||
// Seed a local admin when no users exist yet, so a plain-HTTP / LAN deployment that
|
||||
// can't use Microsoft OIDC (which requires HTTPS + a matching Entra redirect URI) can
|
||||
// still sign in via the local email/password form. Only fires while the user store is
|
||||
// empty; set Bootstrap__AdminEmail and Bootstrap__AdminPassword to enable.
|
||||
{
|
||||
var bootEmail = app.Configuration["Bootstrap:AdminEmail"];
|
||||
var bootPass = app.Configuration["Bootstrap:AdminPassword"];
|
||||
if (!string.IsNullOrWhiteSpace(bootEmail) && !string.IsNullOrWhiteSpace(bootPass))
|
||||
{
|
||||
var users = app.Services.GetRequiredService<IUserService>();
|
||||
// Seed if this email has no account yet — covers both an empty store and a store
|
||||
// that already holds an Entra-provisioned user from a failed sign-in attempt.
|
||||
if (await users.GetByEmailAsync(bootEmail) is null)
|
||||
{
|
||||
await users.CreateLocalUserAsync(bootEmail, "Administrator", UserRole.Admin, bootPass);
|
||||
Log.Information("Bootstrap: created local admin {Email}.", bootEmail);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Information("Bootstrap: local admin {Email} already present; skipping seed.", bootEmail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||
|
||||
Reference in New Issue
Block a user