Skill v1.0.1
currentLLM-judged scan95/1002 files
version: "1.0.1" name: bwfc-identity-migration description: "WORKFLOW SKILL — Migrate ASP.NET Web Forms Identity and Membership authentication to Blazor Server Identity. Covers OWIN→Core middleware, login/register/logout minimal API endpoints, BWFC login controls, cookie auth under Interactive Server, and role-based authorization. WHEN: \"migrate identity\", \"login page migration\", \"OWIN to core\", \"cookie auth blazor\", \"LoginView migration\". INVOKES: dotnet CLI for identity scaffolding. FOR SINGLE OPERATIONS: use bwfc-migration for markup, bwfc-data-migration for EF."
Web Forms Identity → Blazor Identity Migration
Overview
| Web Forms Auth System | Era | Blazor Migration Path | |
|---|---|---|---|
| ASP.NET Identity (OWIN) | 2013+ | ASP.NET Core Identity (closest match) | |
| ASP.NET Membership | 2005-2013 | Core Identity (schema migration required) | |
| FormsAuthentication | 2002-2005 | Core Identity or cookie auth |
Related: /bwfc-migration (markup), /bwfc-data-migration (EF/architecture)
Critical Rules
- Cookie auth requires HTTP endpoints — login/register/logout MUST use
<form method="post">+ minimal API endpoints. Component event handlers (@onclick) silently fail for cookie operations in interactive mode. - NEVER replace LoginView with AuthorizeView — BWFC
LoginViewusesAuthenticationStateProvidernatively with same template names (AnonymousTemplate,LoggedInTemplate). - DisableAntiforgery required — Blazor forms don't include antiforgery tokens. All auth endpoints must call
.DisableAntiforgery(). - Session-based auth data works —
Session["key"]viaWebFormsPageBase.Session/SessionShim. Don't injectIHttpContextAccessor.
Quick Reference
BWFC Login Controls (drop-in replacements)
<Login />, <LoginName />, <LoginStatus />, <LoginView>, <CreateUserWizard />, <ChangePassword />, <PasswordRecovery />
Auth Endpoint Pattern
// Program.cs — login via HTTP POST (required for cookie auth)app.MapPost("/Account/LoginHandler", async (HttpContext ctx, SignInManager<IdentityUser> sm) =>{var form = await ctx.Request.ReadFormAsync();var result = await sm.PasswordSignInAsync(form["email"]!, form["password"]!, false, false);return result.Succeeded ? Results.Redirect("/") : Results.Redirect("/Account/Login?error=failed");}).DisableAntiforgery();
Companion Documents
| Document | Content | |
|---|---|---|
| [IDENTITY-PATTERNS.md](IDENTITY-PATTERNS.md) | Cookie auth details, identity config steps, BWFC login controls, authorization patterns, endpoint templates, OWIN mapping, gotchas |
L2 Break-Fix Playbook
Step 1: Add Identity Packages
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCoredotnet add package Microsoft.AspNetCore.Identity.UI
Step 2: Configure Identity in Program.cs
// Program.csbuilder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>{options.SignIn.RequireConfirmedAccount = false;options.Password.RequiredLength = 6;}).AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();// Required for Blazor Serverbuilder.Services.AddCascadingAuthenticationState();// Middleware pipeline (ORDER MATTERS)app.UseAuthentication();app.UseAuthorization();
Step 3: Create ApplicationUser and DbContext
// Models/ApplicationUser.csusing Microsoft.AspNetCore.Identity;public class ApplicationUser : IdentityUser{// Add custom properties from your Web Forms ApplicationUser}// Data/ApplicationDbContext.csusing Microsoft.AspNetCore.Identity.EntityFrameworkCore;using Microsoft.EntityFrameworkCore;public class ApplicationDbContext : IdentityDbContext<ApplicationUser>{public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options): base(options) { }}
Step 4: Migrate the Database Schema
If migrating from ASP.NET Identity (OWIN), the schema is similar but not identical:
dotnet ef migrations add IdentityMigrationdotnet ef database update
Key schema differences:
| ASP.NET Identity (OWIN) | ASP.NET Core Identity | |
|---|---|---|
AspNetUsers.Id (string GUID) | Same | |
AspNetUsers.PasswordHash | Same format — passwords are compatible | |
AspNetUserClaims | Same | |
AspNetUserRoles | Same | |
AspNetRoles | Same | |
__MigrationHistory | __EFMigrationsHistory |
Important: ASP.NET Identity v2 password hashes (from Web Forms) are compatible with ASP.NET Core Identity. Users will NOT need to reset passwords.
If migrating from Membership (older):
- Use the
Microsoft.AspNetCore.Identity.MicrosoftAccountMigrationor a custom SQL migration script - Membership password hashes are NOT compatible — users will need password resets
Step 5: Migrate Login Pages
BWFC Login Controls
BWFC provides login controls that match Web Forms markup. These controls use native Blazor AuthenticationStateProvider internally — they are not shims and do not need to be replaced with AuthorizeView.
Important: BWFC login controls requirebuilder.Services.AddBlazorWebFormsComponents()inProgram.csandbuilder.Services.AddCascadingAuthenticationState()for authentication state propagation.
| Web Forms | BWFC | Notes | |
|---|---|---|---|
<asp:Login runat="server" /> | <Login /> | Login form with username/password | |
<asp:LoginName runat="server" /> | <LoginName /> | Displays authenticated username | |
<asp:LoginStatus runat="server" /> | <LoginStatus /> | Login/Logout toggle link | |
<asp:LoginView runat="server"> | <LoginView> | Shows different content for anon vs auth users | |
<asp:CreateUserWizard runat="server" /> | <CreateUserWizard /> | Registration form | |
<asp:ChangePassword runat="server" /> | <ChangePassword /> | Password change form | |
<asp:PasswordRecovery runat="server" /> | <PasswordRecovery /> | Password reset flow |
Login Page Migration Example
<!-- Web Forms — Login.aspx --><%@ Page Title="Log in" MasterPageFile="~/Site.Master" ... %><asp:Content ContentPlaceHolderID="MainContent" runat="server"><h2>Log in</h2><asp:Login ID="LoginCtrl" runat="server"ViewStateMode="Disabled"RenderOuterTable="false"><LayoutTemplate><asp:TextBox ID="UserName" runat="server" CssClass="form-control" /><asp:RequiredFieldValidator ControlToValidate="UserName"ErrorMessage="Required" runat="server" /><asp:TextBox ID="Password" TextMode="Password" runat="server" /><asp:Button Text="Log in" CommandName="Login" runat="server" /></LayoutTemplate></asp:Login></asp:Content>
@* Blazor — Login.razor *@@page "/Account/Login"<h2>Log in</h2><Login RenderOuterTable="false"><LayoutTemplate><TextBox @bind-Text="model.UserName" CssClass="form-control" /><RequiredFieldValidator ControlToValidate="UserName" ErrorMessage="Required" /><TextBox TextMode="Password" @bind-Text="model.Password" /><Button Text="Log in" CommandName="Login" /></LayoutTemplate></Login>
Step 6: Migrate Authorization Patterns
Web.config Authorization → Blazor Authorization
<!-- Web Forms — Web.config --><location path="Admin"><system.web><authorization><allow roles="Administrator" /><deny users="*" /></authorization></system.web></location>
@* Blazor — Admin pages use [Authorize] attribute *@@page "/Admin"@attribute [Authorize(Roles = "Administrator")]<h1>Admin Panel</h1>
LoginView Conditional Content
<!-- Web Forms --><asp:LoginView runat="server"><AnonymousTemplate><a href="~/Account/Login">Log in</a></AnonymousTemplate><LoggedInTemplate>Welcome, <asp:LoginName runat="server" />!<asp:LoginStatus runat="server" LogoutAction="Redirect" LogoutPageUrl="~/" /></LoggedInTemplate></asp:LoginView>
@* Blazor — BWFC LoginView (recommended — preserves markup and uses AuthenticationStateProvider natively) *@<LoginView><AnonymousTemplate><a href="/Account/Login">Log in</a></AnonymousTemplate><LoggedInTemplate>Welcome, <LoginName />!<LoginStatus LogoutAction="Redirect" LogoutPageUrl="/" /></LoggedInTemplate></LoginView>
Note: BWFC'sLoginViewinternally injectsAuthenticationStateProviderand evaluates authentication state natively — it is NOT a shim that needs to be replaced. Keep it for markup compatibility. If you prefer native Blazor syntax long-term:
@* Blazor — Native AuthorizeView (alternative — different template names) *@<AuthorizeView><NotAuthorized><a href="/Account/Login">Log in</a></NotAuthorized><Authorized>Welcome, @context.User.Identity?.Name!<a href="/Account/Logout">Log out</a></Authorized></AuthorizeView>
⚠️ NEVER replace LoginView with AuthorizeView. The BWFCLoginViewcomponent is a fully functional Blazor component that injectsAuthenticationStateProvidernatively. It uses the SAME template names asasp:LoginView(AnonymousTemplate,LoggedInTemplate). The migration script handles this automatically — do NOT convert toAuthorizeViewmanually. UseAuthorizeViewonly when you need Blazor-native role-based content (<AuthorizeView Roles="...">) with no Web Forms equivalent.
Role-Based Content
<!-- Web Forms --><asp:LoginView runat="server"><RoleGroups><asp:RoleGroup Roles="Administrator"><ContentTemplate><a href="~/Admin">Admin Panel</a></ContentTemplate></asp:RoleGroup></RoleGroups></asp:LoginView>
@* Blazor *@<AuthorizeView Roles="Administrator"><a href="/Admin">Admin Panel</a></AuthorizeView>
Step 7: Migrate Authentication State Access
Code-Behind Auth Patterns
// Web Forms — code-behindif (HttpContext.Current.User.Identity.IsAuthenticated){var userName = HttpContext.Current.User.Identity.Name;var isAdmin = HttpContext.Current.User.IsInRole("Administrator");}// Web Forms — OWINvar manager = Context.GetOwinContext().GetUserManager<ApplicationUserManager>();var user = manager.FindById(User.Identity.GetUserId());
// Blazor — inject auth state@inject AuthenticationStateProvider AuthStateProvider@code {private string? userName;private bool isAdmin;protected override async Task OnInitializedAsync(){var authState = await AuthStateProvider.GetAuthenticationStateAsync();var user = authState.User;userName = user.Identity?.Name;isAdmin = user.IsInRole("Administrator");}}
Cascading Auth Parameter (Simpler)
// Blazor — cascading parameter (requires AddCascadingAuthenticationState)[CascadingParameter]private Task<AuthenticationState>? AuthState { get; set; }protected override async Task OnInitializedAsync(){if (AuthState != null){var state = await AuthState;var user = state.User;}}
Session-Based Auth Data
Note:WebFormsPageBaseprovides aSessionproperty (viaSessionShim) that works in Blazor Server interactive mode. If your Web Forms code stores auth-related data in session (e.g.,Session["userCheckoutCompleted"],Session["token"],Session["cartId"]), you can access it the same way in migrated Blazor code:```csharp// Web FormsSession["userCheckoutCompleted"] = true;if (Session["token"] is string token) { ... }// Blazor (same pattern, SessionShim handles it)Session["userCheckoutCompleted"] = true;if (Session["token"] is string token) { ... }```SessionShimusesIHttpContextAccessorandISessionunder the hood, but you should not inject `IHttpContextAccessor` directly whenWebFormsPageBase.Sessionalready provides the same access pattern. Keep the shim-based approach during initial migration — session-to-DI conversion is an optional L3 optimization step, not a Layer 1 or Layer 2 requirement.
OWIN Middleware → ASP.NET Core Middleware
| Web Forms (OWIN Startup.cs) | Blazor (Program.cs) | |
|---|---|---|
app.UseCookieAuthentication(...) | builder.Services.AddAuthentication().AddCookie(...) | |
app.UseExternalSignInCookie(...) | builder.Services.AddAuthentication().AddGoogle/Facebook(...) | |
ConfigureAuth(app) in Startup.Auth.cs | Configuration in Program.cs services section | |
app.CreatePerOwinContext<ApplicationUserManager>(...) | builder.Services.AddIdentity<ApplicationUser, IdentityRole>() | |
SecurityStampValidator.OnValidateIdentity(...) | Built into ASP.NET Core Identity automatically |
Ready-to-Use Endpoint Templates
These minimal API endpoints handle authentication operations that must run over HTTP (not WebSocket). Add them to Program.cs after building the app but before app.Run(). Each endpoint reads form data, performs the auth operation, and redirects back to a Blazor page.
Important: Every endpoint below calls.DisableAntiforgery()because Blazor-rendered<form>elements do not include antiforgery tokens. See DisableAntiforgery Requirement.
Login Endpoint
// Program.cs — authenticates user and sets auth cookie via HTTP responseapp.MapPost("/Account/LoginHandler", async (HttpContext context, SignInManager<IdentityUser> signInManager) =>{var form = await context.Request.ReadFormAsync();var email = form["email"].ToString();var password = form["password"].ToString();var result = await signInManager.PasswordSignInAsync(email, password, isPersistent: false, lockoutOnFailure: false);if (result.Succeeded)return Results.Redirect("/");return Results.Redirect("/Account/Login?error=Invalid+login+attempt");}).DisableAntiforgery();
Blazor form that submits to this endpoint:
@page "/Account/Login"<h2>Log in</h2>@if (!string.IsNullOrEmpty(errorMessage)){<div class="text-danger">@errorMessage</div>}<form method="post" action="/Account/LoginHandler"><div><label>Email</label><input type="email" name="email" required /></div><div><label>Password</label><input type="password" name="password" required /></div><button type="submit">Log in</button></form>@code {[SupplyParameterFromQuery] public string? Error { get; set; }private string? errorMessage;protected override void OnInitialized(){errorMessage = Error;}}
Register Endpoint
// Program.cs — creates user, signs in, and redirectsapp.MapPost("/Account/RegisterHandler", async (HttpContext context,UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager) =>{var form = await context.Request.ReadFormAsync();var email = form["email"].ToString();var password = form["password"].ToString();var user = new IdentityUser { UserName = email, Email = email };var result = await userManager.CreateAsync(user, password);if (result.Succeeded){await signInManager.SignInAsync(user, isPersistent: false);return Results.Redirect("/");}var errors = string.Join("; ", result.Errors.Select(e => e.Description));return Results.Redirect($"/Account/Register?error={Uri.EscapeDataString(errors)}");}).DisableAntiforgery();
Logout Endpoint
// Program.cs — signs out and redirects to home pageapp.MapPost("/Account/Logout", async (SignInManager<IdentityUser> signInManager) =>{await signInManager.SignOutAsync();return Results.Redirect("/");}).DisableAntiforgery();
Blazor logout form (use instead of a link):
<form method="post" action="/Account/Logout"><button type="submit" class="nav-link btn btn-link">Log out</button></form>
Note: Do NOT use<a href="/Account/Logout">— Blazor's enhanced navigation will intercept the click and attempt client-side navigation instead of a real HTTP POST. Use a<form method="post">with a submit button, or adddata-enhance-nav="false"to the link. See Blazor Enhanced Navigation in the data migration skill.
DisableAntiforgery Requirement
Important: When using<form method="post">to submit to minimal API endpoints from Blazor-rendered pages, the endpoint MUST call.DisableAntiforgery()because Blazor's HTML rendering does not include antiforgery tokens. Without this, the request will fail with a 400 Bad Request.```csharp// ✅ CORRECT — DisableAntiforgery() required for Blazor form submissionsapp.MapPost("/Account/LoginHandler", handler).DisableAntiforgery();// ❌ WRONG — will reject POST from Blazor-rendered formsapp.MapPost("/Account/LoginHandler", handler);```If you need antiforgery protection, you must manually include the token in the form using@inject Microsoft.AspNetCore.Antiforgery.IAntiforgeryand render a hidden field. For most migration scenarios,.DisableAntiforgery()is the pragmatic choice.
Common Identity Gotchas
No HttpContext.Current
Blazor Server has no HttpContext.Current. Use dependency injection:
// WRONG: HttpContext.Current.User// RIGHT: Inject AuthenticationStateProvider or use [CascadingParameter]
Cookie Auth Requires HTTP Endpoints
Blazor Server login/logout MUST use HTTP endpoints (not component-based), because cookies are set on HTTP responses:
// Program.cs — Add login/logout endpointsapp.MapPost("/Account/Logout", async (SignInManager<ApplicationUser> signInManager) =>{await signInManager.SignOutAsync();return Results.Redirect("/");});
SignalR Circuit vs HTTP Request
Authentication state is captured when the circuit starts. If the user's session expires mid-circuit, they remain "authenticated" until the page refreshes. Use RevalidatingServerAuthenticationStateProvider for periodic revalidation.
Blazor Identity UI Scaffolding
For a complete Identity UI (login, register, manage profile), scaffold it:
dotnet aspnet-codegenerator identity -dc ApplicationDbContext --files "Account.Login;Account.Register;Account.Logout"
This generates Razor Pages (not components) under /Areas/Identity/. They coexist with Blazor.