---
key: security.blazor-removal-auth-posture
title: '2026-07-11 — Blazor removal auth posture: no new exposure beyond phase (a); real auth deferred to #197 (#206)'
status: active
since: '2026-07-11'
supersedes: none
superseded-by: none
rule: 'Removing the Blazor UI''s OIDC-challenged surface exposes nothing a user couldn''t already reach via the already-open `/app` SPA (open since phase (a)); real SPA/API authentication is deliberately deferred to #197, and the removal PR must preserve `ConditionalIptvAuthorizeFilter`, `ApiKeyAuthorizationFilter`, and `JwtHelper` access_token support.'
signals: 'Blazor removal, auth posture sign-off, OIDC attachment point · paths: `ErsatzTV/Startup.cs`, `ErsatzTV/Pages` · issues: #206, #91, #197'
mechanics: '`ErsatzTV/Startup.cs` (Razor Pages/OIDC registration); #91 phase (b) removal PR'
---

Sign-off for the #91 phase (b) removal-gate item #206 ("deleting the last challenged Blazor page leaves
only the open SPA"). The actual authorization wiring in `ErsatzTV/Startup.cs` + `ErsatzTV/Pages` was
enumerated in code (not assumed) before clearing the gate.

**What is gated today**
- **OIDC** (`OidcHelper.IsEnabled` — active only when `Authority`/`ClientId`/`ClientSecret` are configured):
  `AddAuthentication` (cookie default, `oidc` challenge) + `AddAuthorization` `DefaultPolicy =
  RequireAuthenticatedUser` + **`AddRazorPages(… AuthorizeFolder("/"))`** (Startup.cs:379-385) +
  `blazor.UseAuthentication()/UseAuthorization()` inside the Blazor `MapWhen` branch (Startup.cs:764-770).
  `AuthorizeFolder("/")` gates **Razor Pages only**, and the sole user-facing Razor Page is
  `Pages/_Host.cshtml` — the Blazor Server host (the other `.cshtml`, `Shared/_Favicons.cshtml`, is a
  cosmetic partial). **So the OIDC challenge protects exactly the Blazor UI and nothing else.**
- **`/app` (SPA)** is served by its own `MapWhen(path=/app)` static-file branch (Startup.cs:701-714) with
  **no authentication/authorization middleware** — open since phase (a) (`/`→`/app`, PR #148).
- **`/api/*` controllers** carry no `[Authorize]` (verified: zero attributes in `Controllers/`); the
  Razor-Pages `AuthorizeFolder`/`DefaultPolicy` never reach them. Their only optional gate is the
  per-endpoint `ApiKeyAuthorizationFilter` (API-key on mutating JSON endpoints), independent of OIDC/Blazor.
- **`/iptv/*`** is gated by `ConditionalIptvAuthorizeFilter` (JWT `JwtOnlyScheme`, active only when
  `JwtHelper.IsEnabled`) in its own `MapWhen` branch (Startup.cs:797-803) — independent of Blazor.

**Posture after Blazor removal.** Removing `Pages/_Host.cshtml`, `AddRazorPages`/`AuthorizeFolder("/")`,
`blazor.UseAuthentication/UseAuthorization`, `MapBlazorHub`, and `MapFallbackToPage("/_Host")` deletes the
OIDC challenge's **only attachment point** — no user-facing surface remains challenged. **No capability is
lost:** every Blazor-served capability already has an open SPA equivalent (the #91 parity effort), and the
SPA was already the unauthenticated path since phase (a), so removal exposes nothing a user could not already
reach via `/app`.

**The one honest caveat (not a regression introduced by removal):** an OIDC-configured operator's *Blazor*
admin UI sits behind a login today; after removal there is no login-gated admin UI at all (the SPA admin UI
is open). That exposure delta already happened at **phase (a)** (the open SPA became the default admin
surface); removal only deletes the now-redundant challenged duplicate. Designing real SPA/API authentication
is deliberately deferred to **#197** (cold API security review — a HARD GATE before any remote exposure).

**Removal-PR must-not-break (independent gates that survive):** `ConditionalIptvAuthorizeFilter` (`/iptv/*`
JWT), `ApiKeyAuthorizationFilter` (mutating `/api/*`), and `JwtHelper` access_token query support. **Leave
the OIDC service registrations in place** (conditional on config, inert once no Razor Page consumes them) —
ripping OIDC out is a #197 decision, not a removal-PR one. The removal PR removes only the Blazor-attached
pieces above; `MapControllers()` + `/docs` (Scalar), currently co-hosted in the Blazor `MapWhen` branch, must
survive the surgical reduction.
