From 8dfbf7c18a7264aa6a007d7a759c869c93e22780 Mon Sep 17 00:00:00 2001 From: kawa Date: Tue, 9 Jun 2026 17:21:46 +0200 Subject: [PATCH] Fix OIDC stuck-on-loading: slim auth cookie principal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OIDC OnTokenValidated handler stored the raw principal (all id_token + userinfo claims) in the auth cookie. Encrypted + base64 it exceeds ~4 KB, so ChunkingCookieManager splits it across …CookiesC1/C2. The chunked cookie survives the prerender GET but is dropped on the Blazor interactive WebSocket upgrade, so the circuit comes up anonymous and the page sticks on "Chargement…". SaveTokens=false alone didn't shrink it enough — the claims themselves bloat it. Replace the principal with a slim 4-claim identity (preferred_username, name, app_role, auth_provider), identical to the local-login path, so the cookie stays single + unchunked and the circuit authenticates. Also fixes a latent bug: the OIDC principal never carried app_role or auth_provider, so Entra admins got no admin nav and logout skipped the OIDC sign-out branch. Co-Authored-By: Claude Opus 4.8 (1M context) --- Program.cs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/Program.cs b/Program.cs index 1b8c944..9321fab 100644 --- a/Program.cs +++ b/Program.cs @@ -131,7 +131,29 @@ else options.Events.OnTokenValidated = async ctx => { var userService = ctx.HttpContext.RequestServices.GetRequiredService(); - await userService.ProvisionAsync(ctx.Principal!); + var user = await userService.ProvisionAsync(ctx.Principal!); + + // The whole principal is serialized into the auth cookie. The raw OIDC principal carries + // dozens of id_token + userinfo claims (oid, tid, given/family_name, a long picture URL …); + // encrypted + base64 it exceeds ~4 KB, so ChunkingCookieManager splits it into …CookiesC1/C2. + // The chunked cookie survives the prerender GET but is dropped on the Blazor WebSocket upgrade + // → the interactive circuit comes up anonymous → page sticks on "Chargement…". Replace it with + // a slim principal holding only the claims the app reads — identical to the local-login path — + // so the cookie stays small (single, unchunked) and the circuit authenticates. This also adds + // the app_role claim (role-based authz) and auth_provider (logout's OIDC sign-out branch), + // which the fat OIDC principal never had. + var identity = new ClaimsIdentity( + new Claim[] + { + new("preferred_username", user.Email), + new("name", user.DisplayName), + new("app_role", user.Role.ToString()), + new("auth_provider", nameof(AuthProvider.Entra)), + }, + ctx.Principal!.Identity!.AuthenticationType, + "preferred_username", + "app_role"); + ctx.Principal = new ClaimsPrincipal(identity); }; }); }