ASP.NET CORE MVC - Roles and Administrator Authorization

ASP.NET CORE MVC TUTORIAL SERIES · PART 13

Adding Roles and Administrator Authorization

Create an Admin role, assign an existing Identity user to that role, protect an administrator-only page with [Authorize(Roles = "Admin")], and extend Product authorization so administrators can manage all Products while normal users remain limited to their own records.

Objective

By the end of this tutorial, normal users can manage only Products they own, while users in the Admin role can manage the entire Product catalogue and access an administrator-only page.

Starting point

Part 12 introduced ownership-based authorization. A normal user may Edit or Delete a Product only when Product.OwnerId matches the current Identity user ID. Part 13 adds a second authorization path for administrators.

In this tutorial
  1. Understand role-based authorization
  2. Enable role services in Program.cs
  3. Configure the administrator email
  4. Create an Identity role seeder
  5. Create the Admin role and assign a user
  6. Create an administrator-only controller and view
  7. Add an Admin navigation link
  8. Extend Product Edit/Delete authorization
  9. Update Product management links
  10. Test normal-user and administrator access
  11. Verify roles in SQLite
  12. Compare full final files in the appendix

1. Open and Verify the Part 12 Project

cd ~/aspnet-mvc-tutorial/ProductManagement
pwd
dotnet build
code .

The expected path is:

/home/xubuntu/aspnet-mvc-tutorial/ProductManagement

2. Understand Role-Based Authorization

A role represents a named group of permissions or responsibilities. In this tutorial we will use:

Admin
Normal User ↓ Own Products Only Administrator ↓ All Products + Admin-only page

ASP.NET Core can check a role with:

[Authorize(Roles = "Admin")]

3. Enable Role Services in Program.cs

Open:

code Program.cs

Find the existing Identity configuration:

builder.Services
    .AddDefaultIdentity<IdentityUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = false;
    })
    .AddEntityFrameworkStores<ApplicationDbContext>();

Replace it with:

builder.Services
    .AddDefaultIdentity<IdentityUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = false;
    })
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

4. Does Part 13 Need a Migration?

No. The Identity schema created in Part 11 already contains role tables such as AspNetRoles and AspNetUserRoles. Adding role services uses those existing tables.

Do not create an unnecessary migration

You do not need dotnet ef migrations add just because AddRoles<IdentityRole>() was added.

5. Choose the Existing Administrator Account

The administrator account must already exist. Use an account registered in Part 11 or later. For this tutorial, the example is:

admin@example.com

Replace that address with your real registered tutorial account.

6. Update appsettings.json

Open:

code appsettings.json

Add an AdminUser section:

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=ProductManagement.db"
  },
  "AdminUser": {
    "Email": "admin@example.com"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}
No password is stored here

The configuration contains only the email used to locate an already registered user. Do not store the user's password in appsettings.json.

7. Create Data/IdentitySeedData.cs

touch Data/IdentitySeedData.cs
code Data/IdentitySeedData.cs

Add:

using Microsoft.AspNetCore.Identity;

namespace ProductManagement.Data;

public static class IdentitySeedData
{
    public static async Task InitializeAsync(
        IServiceProvider serviceProvider,
        IConfiguration configuration)
    {
        var roleManager =
            serviceProvider.GetRequiredService<
                RoleManager<IdentityRole>>();

        var userManager =
            serviceProvider.GetRequiredService<
                UserManager<IdentityUser>>();

        const string adminRole = "Admin";

        if (!await roleManager.RoleExistsAsync(adminRole))
        {
            var roleResult = await roleManager.CreateAsync(
                new IdentityRole(adminRole));

            if (!roleResult.Succeeded)
            {
                throw new InvalidOperationException(
                    "Could not create the Admin role.");
            }
        }

        var adminEmail =
            configuration["AdminUser:Email"];

        if (string.IsNullOrWhiteSpace(adminEmail))
        {
            return;
        }

        var adminUser =
            await userManager.FindByEmailAsync(adminEmail);

        if (adminUser == null)
        {
            Console.WriteLine(
                $"Admin user '{adminEmail}' was not found.");
            return;
        }

        if (!await userManager.IsInRoleAsync(
                adminUser,
                adminRole))
        {
            var addRoleResult =
                await userManager.AddToRoleAsync(
                    adminUser,
                    adminRole);

            if (!addRoleResult.Succeeded)
            {
                throw new InvalidOperationException(
                    "Could not assign the Admin role.");
            }
        }
    }
}

8. Run the Seeder from Program.cs

Open:

code Program.cs

Find:

var app = builder.Build();

Immediately after it, add:

using (var scope = app.Services.CreateScope())
{
    await IdentitySeedData.InitializeAsync(
        scope.ServiceProvider,
        app.Configuration);
}

9. Build and Run the Seeder

dotnet build
dotnet run

The startup code creates the Admin role if needed and assigns it to the configured user if that user exists.

10. Verify the Role in SQLite

sqlite3 ProductManagement.db
SELECT Id, Name FROM AspNetRoles;

SELECT
    AspNetUsers.UserName,
    AspNetRoles.Name
FROM AspNetUserRoles
JOIN AspNetUsers
    ON AspNetUserRoles.UserId = AspNetUsers.Id
JOIN AspNetRoles
    ON AspNetUserRoles.RoleId = AspNetRoles.Id;

.quit

11. Create an Admin-Only Controller

touch Controllers/AdminController.cs
code Controllers/AdminController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace ProductManagement.Controllers;

[Authorize(Roles = "Admin")]
public class AdminController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}

12. Create the Admin View

mkdir -p Views/Admin
touch Views/Admin/Index.cshtml
code Views/Admin/Index.cshtml
@{
    ViewData["Title"] = "Administration";
}

<h1>Administration</h1>

<p>
    This page is available only to users
    in the Admin role.
</p>

<p>
    <a asp-controller="Products"
       asp-action="Index"
       class="btn btn-primary">
        Manage Products
    </a>
</p>

13. Add an Admin Link to _Layout.cshtml

Open:

code Views/Shared/_Layout.cshtml

Inside the main navigation list, after the Products link, add:

@if (User.IsInRole("Admin"))
{
    <li class="nav-item">
        <a class="nav-link text-dark"
           asp-area=""
           asp-controller="Admin"
           asp-action="Index">
            Admin
        </a>
    </li>
}

14. Update Product Authorization for Admin Override

Open:

code Controllers/ProductsController.cs

In Edit GET, Edit POST, Delete GET and Delete POST, find:

if (product.OwnerId != userId)
{
    return Forbid();
}

Replace each occurrence with:

if (product.OwnerId != userId
    && !User.IsInRole("Admin"))
{
    return Forbid();
}

15. Update Index.cshtml Management Links

Open:

code Views/Products/Index.cshtml

Find the condition that shows Edit/Delete only to the owner. Replace it with:

@if (User.Identity?.IsAuthenticated == true
    && (
        product.OwnerId == UserManager.GetUserId(User)
        || User.IsInRole("Admin")
    ))
{
    <text> | </text>

    <a asp-action="Edit"
       asp-route-id="@product.Id">
        Edit
    </a>

    <text> | </text>

    <a asp-action="Delete"
       asp-route-id="@product.Id">
        Delete
    </a>
}

16. Test Normal User Access

Run:

dotnet run

Log in as a normal user and test:

/Admin

The normal user should be denied. The user should still be able to Edit/Delete only Products they own.

17. Test Administrator Access

Log out and sign in using the account configured under AdminUser:Email. If the role was assigned while the user was already logged in, log out and log in again so the authentication cookie is refreshed.

Test:

/Admin

The page should load. The Admin link should also appear in the navbar.

18. Test Administrator Product Management

  1. Open All Products.
  2. Find a Product owned by another user.
  3. Confirm Edit/Delete links are visible.
  4. Edit the Product.
  5. Try a legacy Product with OwnerId = NULL, if one exists.
  6. Confirm Admin can manage it.

19. Role and Ownership Authorization Model

Authenticated user ↓ Requested Product ↓ Is Owner? ┌───┴───┐ Yes No ↓ ↓ Allow Is Admin? ┌──┴──┐ Yes No ↓ ↓ Allow Forbid

20. Troubleshooting

RoleManager cannot be resolved

Open Program.cs and confirm the Identity configuration contains:

.AddRoles<IdentityRole>()
Admin role exists but the user is not assigned

Confirm the email in appsettings.json matches an existing registered user, then restart the application.

User.IsInRole("Admin") remains false

Log out and log in again after role assignment so the authentication cookie and claims are refreshed.

Admin cannot manage another user's Product

Check all four server-side owner checks. They must include the Admin bypass.

21. Hands-On Exercise

  1. Confirm Admin exists in AspNetRoles.
  2. Confirm one registered user belongs to Admin.
  3. Log in as a normal user and verify /Admin is denied.
  4. Log in as Admin and verify /Admin opens.
  5. Create a Product as a normal user.
  6. Log in as Admin and edit that Product.
  7. Log back in as the normal user and verify another user's Product is still protected.

22. Knowledge Check

  1. What is a role?
  2. Why is AddRoles<IdentityRole>() required?
  3. What does [Authorize(Roles = "Admin")] do?
  4. Why is no migration required in Part 13?
  5. What does RoleManager<IdentityRole> manage?
  6. What does UserManager.AddToRoleAsync() do?
  7. Why may an administrator need to log out and back in after role assignment?
  8. How does Product authorization combine ownership and Admin privilege?

23. Part 13 Summary

  • enabled role services with AddRoles<IdentityRole>();
  • created and seeded the Admin role;
  • assigned an existing user to Admin;
  • created an Admin-only controller and view;
  • used [Authorize(Roles = "Admin")];
  • added an Admin navbar link;
  • extended Product owner checks with an Admin override; and
  • kept normal users restricted to their own Products.

Appendix — Full Code for Final Verification

Appendix A — 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;
    })
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    await IdentitySeedData.InitializeAsync(
        scope.ServiceProvider,
        app.Configuration);
}

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 B — appsettings.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=ProductManagement.db"
  },
  "AdminUser": {
    "Email": "admin@example.com"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Appendix C — Data/IdentitySeedData.cs

using Microsoft.AspNetCore.Identity;

namespace ProductManagement.Data;

public static class IdentitySeedData
{
    public static async Task InitializeAsync(
        IServiceProvider serviceProvider,
        IConfiguration configuration)
    {
        var roleManager =
            serviceProvider.GetRequiredService<
                RoleManager<IdentityRole>>();

        var userManager =
            serviceProvider.GetRequiredService<
                UserManager<IdentityUser>>();

        const string adminRole = "Admin";

        if (!await roleManager.RoleExistsAsync(adminRole))
        {
            var roleResult = await roleManager.CreateAsync(
                new IdentityRole(adminRole));

            if (!roleResult.Succeeded)
            {
                throw new InvalidOperationException(
                    "Could not create the Admin role.");
            }
        }

        var adminEmail =
            configuration["AdminUser:Email"];

        if (string.IsNullOrWhiteSpace(adminEmail))
        {
            return;
        }

        var adminUser =
            await userManager.FindByEmailAsync(adminEmail);

        if (adminUser == null)
        {
            Console.WriteLine(
                $"Admin user '{adminEmail}' was not found.");
            return;
        }

        if (!await userManager.IsInRoleAsync(
                adminUser,
                adminRole))
        {
            var addRoleResult =
                await userManager.AddToRoleAsync(
                    adminUser,
                    adminRole);

            if (!addRoleResult.Succeeded)
            {
                throw new InvalidOperationException(
                    "Could not assign the Admin role.");
            }
        }
    }
}

Appendix D — Controllers/AdminController.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace ProductManagement.Controllers;

[Authorize(Roles = "Admin")]
public class AdminController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}

Appendix E — Views/Admin/Index.cshtml

@{
    ViewData["Title"] = "Administration";
}

<h1>Administration</h1>

<p>
    This page is available only to users
    in the Admin role.
</p>

<p>
    <a asp-controller="Products"
       asp-action="Index"
       class="btn btn-primary">
        Manage Products
    </a>
</p>

Appendix F — Product Authorization Rule

if (product.OwnerId != userId
    && !User.IsInRole("Admin"))
{
    return Forbid();
}

Appendix G — Final Verification Commands

cd ~/aspnet-mvc-tutorial/ProductManagement

dotnet build
dotnet run

Browser checks:

/Products
/Admin

SQLite checks:

sqlite3 ProductManagement.db

SELECT Id, Name
FROM AspNetRoles;

SELECT
    AspNetUsers.UserName,
    AspNetRoles.Name
FROM AspNetUserRoles
JOIN AspNetUsers
    ON AspNetUserRoles.UserId = AspNetUsers.Id
JOIN AspNetRoles
    ON AspNetUserRoles.RoleId = AspNetRoles.Id;

.quit
Final Part 13 checkpoint

If the Admin role exists, the configured user belongs to it, the Admin page is blocked for normal users, administrators can Edit/Delete any Product, and normal users remain limited to their own Products, Part 13 is complete.

Next: Part 14 — Layouts, Partial Views and Reusable UI

Part 14 will improve the structure of the user interface using _Layout.cshtml, partial views, reusable navigation components, Bootstrap styling, and authentication-aware UI.