Identity & Authentication Migration Skill
April 20, 2026 ยท View on GitHub
The Identity & Authentication Migration Skill guides the migration of ASP.NET Web Forms authentication and authorization systems to Blazor Server using ASP.NET Core Identity.
When to Use This Skill
Use this skill when migrating:
- Login pages (
Login.aspx) - Registration pages (
Register.aspx) - Account management pages (password reset, profile)
- ASP.NET Identity (OWIN-based)
- ASP.NET Membership (pre-2013)
- FormsAuthentication patterns
- Role-based authorization (
[Authorize(Roles = "Admin")]) - BWFC login controls (Login, LoginView, ChangePassword, CreateUserWizard)
Web Forms Authentication Systems
Web Forms applications typically use one of three authentication systems:
| System | Era | Database Schema | Password Hash Compatibility |
|---|---|---|---|
| ASP.NET Identity (OWIN) | 2013+ | AspNetUsers, AspNetRoles | Compatible with ASP.NET Core Identity |
| ASP.NET Membership | 2005-2013 | aspnet_Users, aspnet_Membership | Requires migration script |
| FormsAuthentication | 2002-2005 | Custom tables | Requires custom migration |
Critical: Cookie Auth Under Interactive Server Mode
** IMPORTANT:** When using global interactive server mode (<Routes @rendermode="InteractiveServer" />), HttpContext is NULL during WebSocket circuits. This means:
SignInAsync()does not work in component event handlersSignOutAsync()does not work in component event handlers- Cookies cannot be set from
@onclickhandlers
Required Pattern: Minimal API Endpoints
Cookie-based authentication operations (login, register, logout) must use HTML <form method="post"> that submits to minimal API endpoints:
@* Login.razor form posts to endpoint, NOT a Blazor event handler *@
<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>
// Program.cs minimal API endpoint performs SignInAsync
app.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);
return result.Succeeded
? Results.Redirect("/")
: Results.Redirect("/Account/Login?error=Invalid+login+attempt");
}).DisableAntiforgery(); // Required Blazor forms don't include antiforgery tokens
Why .DisableAntiforgery() is required:
Blazor's HTML rendering does not automatically include <input type="hidden" name="__RequestVerificationToken" /> in <form> elements. Without disabling antiforgery validation, the POST will fail with a 400 Bad Request error.
Migration Paths
Path 1: ASP.NET Identity (OWIN) ASP.NET Core Identity
Best fit when:
- Original app uses
Microsoft.AspNet.Identity.EntityFramework Web.confighas<add key="owin:appStartup" value="Startup" />- Database has
AspNetUsers,AspNetRoles,AspNetUserClaimstables
Steps:
-
Install packages:
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore dotnet add package Microsoft.AspNetCore.Identity.UI -
Create ApplicationUser and DbContext:
public class ApplicationUser : IdentityUser { // Add custom properties from your Web Forms ApplicationUser } public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } } -
Configure Identity in Program.cs:
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options => { options.SignIn.RequireConfirmedAccount = false; options.Password.RequiredLength = 6; }) .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); builder.Services.AddCascadingAuthenticationState(); // Middleware pipeline (ORDER MATTERS) app.UseAuthentication(); app.UseAuthorization(); -
Migrate database schema:
dotnet ef migrations add IdentityMigration dotnet ef database update
Password hash compatibility: ASP.NET Identity v2 (Web Forms) password hashes are compatible with ASP.NET Core Identity. Users can log in with existing passwords.
Path 2: ASP.NET Membership ASP.NET Core Identity
Best fit when:
- Original app uses
System.Web.Security.Membership - Database has
aspnet_Users,aspnet_Membership,aspnet_Rolestables - No OWIN middleware
Steps:
-
Install packages (same as Path 1)
-
Migrate schema using SQL script:
-- Example migration from Membership to ASP.NET Core Identity -- (Use Microsoft's migration tool or custom script) INSERT INTO AspNetUsers (Id, UserName, Email, EmailConfirmed, PasswordHash, SecurityStamp) SELECT CAST(UserId AS NVARCHAR(450)), LoweredUserName, LoweredEmail, 1, Password, -- Hash format is NOT compatible NEWID() FROM aspnet_Membership m JOIN aspnet_Users u ON m.UserId = u.UserId; -
Force password resets: Because Membership password hashes are not compatible, users must reset passwords on first login.
Path 3: FormsAuthentication Cookie Authentication
Best fit when:
- Original app uses
FormsAuthentication.SetAuthCookie() - Custom user tables (not AspNet* schema)
- Lightweight auth needs
Steps:
-
Configure cookie authentication:
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.LoginPath = "/Account/Login"; options.LogoutPath = "/Account/Logout"; }); -
Create login minimal API:
app.MapPost("/Account/LoginHandler", async (HttpContext context, IUserService userService) => { var form = await context.Request.ReadFormAsync(); var username = form["username"].ToString(); var password = form["password"].ToString(); var user = await userService.ValidateCredentialsAsync(username, password); if (user == null) return Results.Redirect("/Account/Login?error=Invalid+credentials"); var claims = new List<Claim> { new Claim(ClaimTypes.Name, user.Username), new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()) }; var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); var principal = new ClaimsPrincipal(identity); await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal); return Results.Redirect("/"); }).DisableAntiforgery();
BWFC Login Controls
BWFC provides Blazor equivalents of Web Forms login controls:
| Web Forms Control | BWFC Component | Status |
|---|---|---|
<asp:Login> | <Login> | Supported |
<asp:LoginView> | <LoginView> | Supported |
<asp:LoginStatus> | <LoginStatus> | Supported |
<asp:LoginName> | <LoginName> | Supported |
<asp:ChangePassword> | <ChangePassword> | Supported |
<asp:CreateUserWizard> | <CreateUserWizard> | Supported |
<asp:PasswordRecovery> | <PasswordRecovery> | Supported |
Example Migration:
<!-- Web Forms -->
<asp:LoginView runat="server">
<AnonymousTemplate>
<asp:Login ID="LoginControl" runat="server" />
</AnonymousTemplate>
<LoggedInTemplate>
Welcome, <asp:LoginName runat="server" />!
<asp:LoginStatus runat="server" LogoutAction="Redirect" LogoutPageUrl="~/" />
</LoggedInTemplate>
</asp:LoginView>
<!-- Blazor with BWFC -->
<LoginView>
<NotAuthorized>
<Login />
</NotAuthorized>
<Authorized>
Welcome, <LoginName />!
<LoginStatus LogoutAction="Redirect" LogoutPageUrl="/" />
</Authorized>
</LoginView>
Role-Based Authorization
Web Forms:
<%@ Page ... %>
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
if (!User.IsInRole("Admin"))
{
Response.Redirect("~/AccessDenied.aspx");
}
}
</script>
Blazor with BWFC:
@page "/Admin"
@attribute [Authorize(Roles = "Admin")]
<h1>Admin Dashboard</h1>
Or use <AuthorizeView>:
<AuthorizeView Roles="Admin">
<Authorized>
<h1>Admin Dashboard</h1>
</Authorized>
<NotAuthorized>
<p>Access denied.</p>
</NotAuthorized>
</AuthorizeView>
Common Migration Scenarios
Scenario 1: Simple Login Page
Before (Web Forms):
<asp:Login ID="LoginControl"
OnAuthenticate="LoginControl_Authenticate"
DestinationPageUrl="~/"
runat="server" />
protected void LoginControl_Authenticate(object sender, AuthenticateEventArgs e)
{
var username = LoginControl.UserName;
var password = LoginControl.Password;
e.Authenticated = Membership.ValidateUser(username, password);
}
After (Blazor with minimal API):
@page "/Account/Login"
<form method="post" action="/Account/LoginHandler">
<div>
<label>Username</label>
<input type="text" name="username" required />
</div>
<div>
<label>Password</label>
<input type="password" name="password" required />
</div>
<button type="submit">Log in</button>
</form>
// Program.cs
app.MapPost("/Account/LoginHandler", async (HttpContext context, SignInManager<ApplicationUser> signInManager) =>
{
var form = await context.Request.ReadFormAsync();
var username = form["username"].ToString();
var password = form["password"].ToString();
var result = await signInManager.PasswordSignInAsync(username, password, isPersistent: false, lockoutOnFailure: false);
return result.Succeeded
? Results.Redirect("/")
: Results.Redirect("/Account/Login?error=1");
}).DisableAntiforgery();
Scenario 2: Registration Page
Before (Web Forms):
<asp:CreateUserWizard ID="CreateUserWizard1" runat="server" OnCreatedUser="CreateUserWizard1_CreatedUser">
<WizardSteps>
<asp:CreateUserWizardStep runat="server" />
<asp:CompleteWizardStep runat="server" />
</WizardSteps>
</asp:CreateUserWizard>
After (Blazor with minimal API):
@page "/Account/Register"
<form method="post" action="/Account/RegisterHandler">
<div>
<label>Email</label>
<input type="email" name="email" required />
</div>
<div>
<label>Password</label>
<input type="password" name="password" required />
</div>
<div>
<label>Confirm Password</label>
<input type="password" name="confirmPassword" required />
</div>
<button type="submit">Register</button>
</form>
// Program.cs
app.MapPost("/Account/RegisterHandler", async (HttpContext context, UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager) =>
{
var form = await context.Request.ReadFormAsync();
var email = form["email"].ToString();
var password = form["password"].ToString();
var confirmPassword = form["confirmPassword"].ToString();
if (password != confirmPassword)
return Results.Redirect("/Account/Register?error=passwords");
var user = new ApplicationUser { UserName = email, Email = email };
var result = await userManager.CreateAsync(user, password);
if (result.Succeeded)
{
await signInManager.SignInAsync(user, isPersistent: false);
return Results.Redirect("/");
}
return Results.Redirect("/Account/Register?error=failed");
}).DisableAntiforgery();
How to Use This Skill
In Copilot Chat
@workspace Use the identity migration skill to convert the Login.aspx page
to Blazor. The original uses ASP.NET Identity with OWIN.
Pattern-Specific Questions
Using the BWFC identity migration patterns, how should I handle cookie
authentication in Interactive Server mode? The original uses FormsAuthentication.
Related Skills
- Core Migration For markup and code-behind transforms
- Data & Architecture Migration For database and architecture decisions
Skill File Location
The full skill file is located at:
migration-toolkit/skills/bwfc-identity-migration/SKILL.md