ASP.NET Core Identity
Add User Registration and Login with ASP.NET Core Identity
Add ASP.NET Core Identity to the existing Product Management application, create the Identity database tables, enable registration, login and logout, and protect Product management actions with authentication.
By the end of this tutorial, users will be able to register and log in. Anonymous visitors will still be able to view the Product catalogue, but Create, Edit and Delete operations will require an authenticated account.
This tutorial continues from the completed Part 10 project. The application already uses EF Core and SQLite through ApplicationDbContext. We will extend that existing context to support ASP.NET Core Identity rather than creating an unrelated second application database.
- Understand authentication and authorization
- Install the Identity EF Core package
- Update
ApplicationDbContext - Configure Identity in
Program.cs - Add authentication middleware
- Add Identity Razor Pages
- Create and apply the Identity migration
- Add Register/Login/Logout navigation
- Protect Product management actions
- Test anonymous and authenticated access
- Inspect the Identity tables
- Verify the complete final files
1. Open and Verify the Part 10 Project
Open the Xubuntu Terminal:
cd ~/aspnet-mvc-tutorial/ProductManagement
pwd
dotnet build
The expected path is:
/home/xubuntu/aspnet-mvc-tutorial/ProductManagement
Then open the project:
code .
Continue only when the Part 10 project builds successfully.
2. Authentication versus Authorization
These two concepts are related but different.
| Concept | Question | Example |
|---|---|---|
| Authentication | Who are you? | The user logs in with an account. |
| Authorization | What are you allowed to do? | Only authenticated users may create Products. |
3. What Is ASP.NET Core Identity?
ASP.NET Core Identity provides account-management infrastructure including users, password hashing, authentication cookies and related security features.
After this part, the application flow becomes:
Passwords are not stored as plain text. Identity stores password hashes and security-related account information in its database tables.
4. Install the ASP.NET Core Identity Packages
Our existing MVC project was created without Individual Accounts. We therefore need both the EF Core Identity integration and the default Identity UI used for Register, Login and Logout.
Step 4.1 — Install the Identity EF Core package
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore --version 8.0.0
This package integrates ASP.NET Core Identity with Entity Framework Core and our ApplicationDbContext.
Step 4.2 — Install the Identity UI package
dotnet add package Microsoft.AspNetCore.Identity.UI --version 8.0.0
This package supplies the default Identity UI infrastructure used by AddDefaultIdentity() and the built-in Register/Login Razor Pages.
Do not omit Microsoft.AspNetCore.Identity.UI. If only the EntityFrameworkCore package is installed, AddDefaultIdentity() may produce compiler error CS1061 because the extension is provided by the Identity UI assembly.
Step 4.3 — Restore and build
dotnet restore
dotnet build
The project should build successfully before you modify ApplicationDbContext.cs. If the build reports an error, resolve it before continuing.
This tutorial series targets .NET 8. Both Identity packages are therefore kept on the 8.x line so that the project does not mix major framework versions.
5. Update ApplicationDbContext.cs
Step 5.1 — Open the file
code Data/ApplicationDbContext.cs
Step 5.2 — Add the Identity EF Core namespace
At the top of the file, add:
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
Step 5.3 — Find the class declaration
It currently begins:
public class ApplicationDbContext : DbContext
Step 5.4 — Replace only that declaration
public class ApplicationDbContext : IdentityDbContext
Do not remove the existing Products, Categories or seed data.
Step 5.5 — Verify the beginning of the file
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using ProductManagement.Models;
namespace ProductManagement.Data;
public class ApplicationDbContext : IdentityDbContext
{
public ApplicationDbContext(
DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
// Existing DbSet properties continue here.
}
6. Why Change DbContext to IdentityDbContext?
IdentityDbContext extends EF Core's DbContext with the entities and configuration required by ASP.NET Core Identity.
Your existing application tables remain in the same SQLite database.
7. Configure Identity Services in Program.cs
Step 7.1 — Open Program.cs
code Program.cs
Step 7.2 — Add the Identity namespace
At the top, add:
using Microsoft.AspNetCore.Identity;
Step 7.3 — Find the DbContext registration
Keep your existing:
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(
builder.Configuration.GetConnectionString(
"DefaultConnection")));
Step 7.4 — Add Identity immediately after the DbContext registration
builder.Services
.AddDefaultIdentity<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>();
Step 7.5 — Add Razor Pages services
Find:
builder.Services.AddControllersWithViews();
Immediately after it, add:
builder.Services.AddRazorPages();
The main Product application still uses MVC controllers and views. Identity's built-in account UI, however, is exposed through Razor Pages under the Identity area.
8. Add Authentication Middleware
Step 8.1 — Keep Program.cs open
Step 8.2 — Find this section
app.UseRouting();
app.UseAuthorization();
Step 8.3 — Add UseAuthentication() before UseAuthorization()
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
The order matters: the application first establishes the user's authenticated identity, then evaluates authorization.
9. Map the Identity Razor Pages
Step 9.1 — Find the MVC route mapping near the bottom of Program.cs
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
Step 9.2 — Add this immediately after it
app.MapRazorPages();
The end of Program.cs should resemble:
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
10. Build Before Creating the Migration
Save ApplicationDbContext.cs and Program.cs, then run:
dotnet build
The project should build successfully before creating the Identity migration.
11. Create the Identity Migration
Unlike Part 9 and Part 10, Part 11 changes the database schema. A migration is therefore required.
dotnet ef migrations add AddIdentity
Then inspect the migration list:
dotnet ef migrations list
You should see the earlier migrations plus:
AddIdentity
12. Apply the Identity Migration
dotnet ef database update
This adds the Identity tables to the existing SQLite database.
13. Inspect the New Identity Tables
First identify the database filename from your connection string if necessary:
cat appsettings.json
If the database is ProductManagement.db, open it:
sqlite3 ProductManagement.db
At the SQLite prompt:
.tables
You should now see Identity tables such as:
AspNetUsers
AspNetRoles
AspNetUserRoles
AspNetUserClaims
AspNetUserLogins
AspNetUserTokens
AspNetRoleClaims
Exit:
.quit
14. Add Login and Registration Navigation
The default MVC project does not automatically contain the Identity navigation partial when Identity is added later, so we will create one.
Step 14.1 — Create the partial view
touch Views/Shared/_LoginPartial.cshtml
code Views/Shared/_LoginPartial.cshtml
Step 14.2 — Add the complete partial
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager
<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
<li class="nav-item">
<span class="nav-link text-dark">
Hello @User.Identity?.Name!
</span>
</li>
<li class="nav-item">
<form asp-area="Identity"
asp-page="/Account/Logout"
asp-route-returnUrl="/"
method="post">
<button type="submit"
class="nav-link btn btn-link text-dark">
Logout
</button>
</form>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark"
asp-area="Identity"
asp-page="/Account/Register">
Register
</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark"
asp-area="Identity"
asp-page="/Account/Login">
Login
</a>
</li>
}
</ul>
15. Add _LoginPartial to _Layout.cshtml
Step 15.1 — Open the layout
code Views/Shared/_Layout.cshtml
Step 15.2 — Find the main navigation list
Look for the closing </div> of the navbar collapse section. In the standard MVC layout, it appears after the existing navigation <ul>.
Step 15.3 — Add the partial after the main navigation list but before the navbar collapse div closes
<partial name="_LoginPartial" />
The relevant section should resemble:
<div class="navbar-collapse collapse d-sm-inline-flex
justify-content-between">
<ul class="navbar-nav flex-grow-1">
...
</ul>
<partial name="_LoginPartial" />
</div>
Part 11 only requires inserting the login partial into the existing navbar. Keep the rest of your Part 10 layout unchanged.
16. Run the Application
dotnet run
Open the application in the browser. The navigation should now display:
Register
Login
17. Register a User
Click Register. Identity should route to:
/Identity/Account/Register
Enter an email address and password and submit the form.
We set RequireConfirmedAccount = false so this tutorial does not require an email-confirmation service. Production systems normally require a more deliberate account-confirmation and recovery configuration.
18. Verify the User in SQLite
Stop the application if needed, then open SQLite:
sqlite3 ProductManagement.db
Run:
SELECT Id, UserName, Email
FROM AspNetUsers;
The registered user should appear. Then exit:
.quit
19. Protect Product Management Actions
For this tutorial, everyone may view the Product list and Product details. Only authenticated users may Create, Edit or Delete.
| Action | Anonymous visitor | Logged-in user |
|---|---|---|
| Index | Allowed | Allowed |
| Details | Allowed | Allowed |
| Create | Blocked | Allowed |
| Edit | Blocked | Allowed |
| Delete | Blocked | Allowed |
20. Add the Authorization Namespace
Step 20.1 — Open ProductsController.cs
code Controllers/ProductsController.cs
Step 20.2 — Add this using statement at the top
using Microsoft.AspNetCore.Authorization;
21. Protect the Create Actions
Step 21.1 — Find Create GET
Add [Authorize] immediately above [HttpGet]:
[Authorize]
[HttpGet]
public async Task<IActionResult> Create()
Step 21.2 — Find Create POST
Add [Authorize] immediately above [HttpPost]:
[Authorize]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
ProductFormViewModel viewModel)
22. Protect the Edit Actions
Add [Authorize] to both Edit actions:
[Authorize]
[HttpGet]
public async Task<IActionResult> Edit(int? id)
and:
[Authorize]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(
int id,
ProductFormViewModel viewModel)
23. Protect the Delete Actions
Add [Authorize] to Delete GET:
[Authorize]
[HttpGet]
public async Task<IActionResult> Delete(int? id)
and Delete POST:
[Authorize]
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
The GET action displays the protected operation and the POST action actually changes the database. Authorization must be enforced by the server on every protected endpoint, not only by hiding buttons in the interface.
24. Build Again
dotnet build
If the build succeeds:
dotnet run
25. Test Anonymous Access
Log out, then test:
/Products
/Products/Details/1
These should remain accessible.
Now test:
/Products/Create
/Products/Edit/1
/Products/Delete/1
Because these actions have [Authorize], an anonymous visitor should be redirected to the Identity login page.
26. Test Authenticated Access
Log in with the account created earlier and test:
/Products/Create
/Products/Edit/1
/Products/Delete/1
The authenticated user should now be allowed to access these actions.
At this stage, any authenticated user can Edit or Delete any Product. Part 12 will solve this by associating each Product with its owner and enforcing ownership-based authorization.
27. Understand the Authentication Cookie
After successful login, Identity uses an authentication cookie. On later requests, ASP.NET Core uses that cookie to reconstruct the authenticated user.
The browser does not repeatedly send the user's password for every protected MVC request.
28. Important Security Concepts Introduced
| Concept | Purpose |
|---|---|
| Password hashing | Avoid storing plain-text passwords. |
| Authentication cookie | Maintain the authenticated session across requests. |
[Authorize] | Require an authenticated user for an action. |
| Anti-forgery validation | Help protect state-changing form submissions against CSRF. |
| Server-side authorization | Prevent protected actions even when someone manually constructs a URL/request. |
29. Troubleshooting
CS1061: AddDefaultIdentity cannot be found
This usually means the default Identity UI package is missing. From the ProductManagement project folder, run:
dotnet add package Microsoft.AspNetCore.Identity.UI --version 8.0.0
dotnet restore
dotnet buildAlso confirm Program.cs contains:
using Microsoft.AspNetCore.Identity;IdentityDbContext cannot be found
Open Data/ApplicationDbContext.cs and confirm:
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;Register or Login returns 404
Open Program.cs and confirm both:
builder.Services.AddRazorPages();and:
app.MapRazorPages();[Authorize] cannot be found
Open Controllers/ProductsController.cs and add:
using Microsoft.AspNetCore.Authorization;SQLite says AspNetUsers does not exist
Confirm the Identity migration was created and applied:
dotnet ef migrations list
dotnet ef database updateRegister works but the navbar does not change
Confirm Views/Shared/_Layout.cshtml contains:
<partial name="_LoginPartial" />and that Views/Shared/_LoginPartial.cshtml exists.
30. Hands-On Exercise
- Open Products while logged out.
- Open Product Details while logged out.
- Attempt to open Create while logged out.
- Register a new user.
- Log in.
- Create a Product.
- Edit a Product.
- Log out.
- Attempt to edit the Product again.
- Inspect
AspNetUsersin SQLite.
31. Knowledge Check
- What is authentication?
- What is authorization?
- What does ASP.NET Core Identity provide?
- Why does
ApplicationDbContextinherit fromIdentityDbContext? - Why is an EF Core migration required in Part 11?
- What does
UseAuthentication()do? - Why must it appear before
UseAuthorization()? - What does
[Authorize]do? - Why must protected POST actions also use authorization?
- What security limitation remains after Part 11?
Show suggested answers
- It establishes who the user is.
- It determines what an identified user is allowed to access or perform.
- User/account management, password hashing, authentication cookies and related identity infrastructure.
- So the application's EF Core context includes Identity's entity and table configuration alongside Products and Categories.
- Identity introduces new database tables.
- It reads authentication information and establishes the current user for the request.
- Authorization needs an established user identity before it can evaluate access.
- It requires the current request to satisfy the configured authorization policy; by default, that means an authenticated user.
- Because the POST action is the endpoint that changes data and cannot rely on UI visibility for security.
- Every authenticated user can still manage every Product; Product ownership has not yet been implemented.
32. Part 11 Summary
- introduced authentication and authorization;
- added ASP.NET Core Identity;
- changed
ApplicationDbContextto inherit fromIdentityDbContext; - configured Identity services;
- enabled authentication middleware;
- mapped Identity Razor Pages;
- created and applied an Identity migration;
- added Register, Login and Logout navigation;
- protected Create, Edit and Delete with
[Authorize]; - inspected Identity data in SQLite; and
- identified the need for ownership-based authorization.
Appendix — Full Code for Final Verification
Use this appendix after completing Part 11. Compare the files modified in this tutorial with the complete versions below. Files not shown here remain unchanged from Part 10.
Appendix A — Data/ApplicationDbContext.cs
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using ProductManagement.Models;
namespace ProductManagement.Data;
public class ApplicationDbContext : IdentityDbContext
{
public ApplicationDbContext(
DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Product> Products { get; set; } = null!;
public DbSet<Category> Categories { get; set; } = null!;
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Category>().HasData(
new Category
{
Id = 1,
Name = "Computers"
},
new Category
{
Id = 2,
Name = "Accessories"
},
new Category
{
Id = 3,
Name = "Storage"
}
);
}
}
Appendix B — Program.cs
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using ProductManagement.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(
builder.Configuration.GetConnectionString(
"DefaultConnection")));
builder.Services
.AddDefaultIdentity<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
Appendix C — Views/Shared/_LoginPartial.cshtml
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager
<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
<li class="nav-item">
<span class="nav-link text-dark">
Hello @User.Identity?.Name!
</span>
</li>
<li class="nav-item">
<form asp-area="Identity"
asp-page="/Account/Logout"
asp-route-returnUrl="/"
method="post">
<button type="submit"
class="nav-link btn btn-link text-dark">
Logout
</button>
</form>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark"
asp-area="Identity"
asp-page="/Account/Register">
Register
</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark"
asp-area="Identity"
asp-page="/Account/Login">
Login
</a>
</li>
}
</ul>
Appendix D — Views/Shared/_Layout.cshtml
Your layout may contain small differences from the original MVC template because of earlier tutorial changes. Do not replace the whole file solely to match this appendix. The important Part 11 change is the _LoginPartial inside the navbar.
Verify the navbar contains:
<div class="navbar-collapse collapse d-sm-inline-flex
justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark"
asp-area=""
asp-controller="Home"
asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark"
asp-area=""
asp-controller="Products"
asp-action="Index">Products</a>
</li>
</ul>
<partial name="_LoginPartial" />
</div>
Appendix E — Controllers/ProductsController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using ProductManagement.Data;
using ProductManagement.Models;
using ProductManagement.ViewModels;
namespace ProductManagement.Controllers;
public class ProductsController : Controller
{
private readonly ApplicationDbContext _context;
public ProductsController(ApplicationDbContext context)
{
_context = context;
}
public async Task<IActionResult> Index(
string? search,
int? categoryId,
decimal? maxPrice,
string? sortOrder)
{
var products = _context.Products
.Include(p => p.Category)
.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
{
products = products.Where(
p => p.Name.Contains(search));
}
if (categoryId.HasValue)
{
products = products.Where(
p => p.CategoryId == categoryId.Value);
}
if (maxPrice.HasValue)
{
products = products.Where(
p => p.Price <= maxPrice.Value);
}
products = sortOrder switch
{
"name_desc" =>
products.OrderByDescending(p => p.Name),
"price" =>
products.OrderBy(p => p.Price),
"price_desc" =>
products.OrderByDescending(p => p.Price),
_ =>
products.OrderBy(p => p.Name)
};
var viewModel = new ProductIndexViewModel
{
Products = await products.ToListAsync(),
Search = search,
CategoryId = categoryId,
MaxPrice = maxPrice,
SortOrder = sortOrder,
NameSort =
sortOrder == "name_desc"
? ""
: "name_desc",
PriceSort =
sortOrder == "price"
? "price_desc"
: "price",
Categories = await _context.Categories
.OrderBy(c => c.Name)
.Select(c => new SelectListItem
{
Value = c.Id.ToString(),
Text = c.Name,
Selected = c.Id == categoryId
})
.ToListAsync()
};
return View(viewModel);
}
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
var product = await _context.Products
.Include(p => p.Category)
.FirstOrDefaultAsync(p => p.Id == id);
if (product == null)
{
return NotFound();
}
return View(product);
}
[Authorize]
[HttpGet]
public async Task<IActionResult> Create()
{
var viewModel = new ProductFormViewModel
{
Categories = await GetCategoryItemsAsync()
};
return View(viewModel);
}
[Authorize]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
ProductFormViewModel viewModel)
{
if (!ModelState.IsValid)
{
viewModel.Categories =
await GetCategoryItemsAsync(
viewModel.CategoryId);
return View(viewModel);
}
var product = new Product
{
Name = viewModel.Name,
Description = viewModel.Description,
Price = viewModel.Price,
Quantity = viewModel.Quantity,
CategoryId = viewModel.CategoryId
};
_context.Products.Add(product);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
[Authorize]
[HttpGet]
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var product = await _context.Products
.FindAsync(id);
if (product == null)
{
return NotFound();
}
var viewModel = new ProductFormViewModel
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
Price = product.Price,
Quantity = product.Quantity,
CategoryId = product.CategoryId,
Categories =
await GetCategoryItemsAsync(
product.CategoryId)
};
return View(viewModel);
}
[Authorize]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(
int id,
ProductFormViewModel viewModel)
{
if (id != viewModel.Id)
{
return NotFound();
}
if (!ModelState.IsValid)
{
viewModel.Categories =
await GetCategoryItemsAsync(
viewModel.CategoryId);
return View(viewModel);
}
var product = await _context.Products
.FindAsync(id);
if (product == null)
{
return NotFound();
}
product.Name = viewModel.Name;
product.Description = viewModel.Description;
product.Price = viewModel.Price;
product.Quantity = viewModel.Quantity;
product.CategoryId = viewModel.CategoryId;
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
[Authorize]
[HttpGet]
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
var product = await _context.Products
.Include(p => p.Category)
.FirstOrDefaultAsync(p => p.Id == id);
if (product == null)
{
return NotFound();
}
return View(product);
}
[Authorize]
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var product = await _context.Products
.FindAsync(id);
if (product != null)
{
_context.Products.Remove(product);
await _context.SaveChangesAsync();
}
return RedirectToAction(nameof(Index));
}
private async Task<List<SelectListItem>>
GetCategoryItemsAsync(int? selectedId = null)
{
return await _context.Categories
.OrderBy(c => c.Name)
.Select(c => new SelectListItem
{
Value = c.Id.ToString(),
Text = c.Name,
Selected = c.Id == selectedId
})
.ToListAsync();
}
private bool ProductExists(int id)
{
return _context.Products.Any(
p => p.Id == id);
}
}
Appendix F — Final Verification Commands
cd ~/aspnet-mvc-tutorial/ProductManagement
dotnet list package
dotnet build
dotnet ef migrations list
dotnet ef database update
dotnet run
Final browser checks:
/Products
/Products/Details/1
/Products/Create
/Identity/Account/Register
/Identity/Account/Login
Final SQLite check:
sqlite3 ProductManagement.db
.tables
SELECT Id, UserName, Email
FROM AspNetUsers;
.quit
If registration, login and logout work; Identity tables exist; anonymous users can view Products but are redirected to Login for Create/Edit/Delete; and authenticated users can access those protected actions, Part 11 is complete.
Part 12 will add an OwnerId to Product, obtain the current authenticated user through Identity, assign ownership when Products are created, filter records by user, and prevent normal users from modifying Products they do not own.