ASP.NET Core 3.1 - Admin Role

Ken Haggerty
Created 02/01/2020 - Updated 02/24/2021 00:16

This article will demonstrate the implementation of an admin role to manage users. I will assume you have downloaded the ASP.NET Core 3.1 - Users Without Identity Project or created a new ASP.NET Core 3.1 Razor Pages project. See Tutorial: Get started with Razor Pages in ASP.NET Core. You should review the earlier articles of the Users Without Identity Project series.

Users Without Identity Project and Article Series

I don't want all users to have access to create, read, update and delete other users. I implemented Authorization with a Role claim, an IsAdmin property for the user and an Admins policy. Add the IsAdmin bool property to the AppUser class.

Edit Entities > AppUser.cs:
public class AppUser
{
    [Key]
    public int Id { get; set; }

    [Required]
    [Display(Name = "Login Name")]
    [StringLength(32, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
    public string LoginName { get; set; }

    [Required]
    [StringLength(32)]
    public string LoginNameUppercase { get; set; }

    [Required]
    [MaxLength(84, ErrorMessage = "The {0} is max {1} characters long.")]
    public string PasswordHash { get; set; }

    [Display(Name = "Admin")]
    public bool IsAdmin { get; set; }
}

Add the AdminRole migration.

Package Manager Console:
add-migration AdminRole -o Data/Migrations
DotNet CLI:
dotnet-ef migrations add AdminRole -o Data/Migrations

The add migration produces a MigrationBuilder which will add the IsAdmin column to the AppUsers table. The file name prepends a date time stamp to AdminRole.

20200120183102_AdminRole.cs:
public partial class AdminRole : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AddColumn<bool>(
            name: "IsAdmin",
            table: "AppUsers",
            nullable: false,
            defaultValue: false);
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropColumn(
            name: "IsAdmin",
            table: "AppUsers");
    }
}

If all looks good, update the database.

Package Manager Console:
update-database
DotNet CLI:
dotnet-ef database update

Add the IsAdmin property to the InputModels for the create and edit user pages.

Edit Admin/ Users/ Create.cshtml.cs && Edit.cshtml.cs > InputModel:
[Display(Name = "Admin")]
public bool IsAdmin { get; set; }

Update the pages with IsAdmin inputs.

Edit Create.cshtml && Edit.cshtml:
<div class="form-group">
    <div class="custom-control custom-checkbox">
        <input class="custom-control-input" asp-for="Input.IsAdmin" />
        <label class="custom-control-label" asp-for="Input.IsAdmin">
            @Html.DisplayNameFor(model => model.Input.IsAdmin)
        </label>
    </div>
    <span asp-validation-for="Input.IsAdmin" class="text-danger"></span>
</div>

Add/Update the IsAdmin property when you save the user. Be sure to add IsAdmin to the new InputModel in OnGet for the edit page. The code for details and delete doesn't need modification but I add the IsAdmin property to the pages.

If the user's IsAdmin = true, add a Role claim to the Claims Principle when the user logs in.

Edit Account/ Login.cshtml.cs > OnPost:
if (user.IsAdmin)
{
    claims.Add(new Claim(ClaimTypes.Role, "Admin"));
}

Update Services/ EnsureAdministrator.cs. Set IsAdmin = true for the new AppUser we automatically create at startup to guarantee an Administrator user.

Because we placed all the user CRUD pages in the Admin/ Users folder, we can use a policy to restrict access to the entire folder.

Edit Startup.cs > ConfigureServices:
services.AddAuthorization(options =>
{
    options.AddPolicy("Admins", policy =>
    {
        policy.RequireRole("Admin");
    });
});

services.AddRazorPages(options =>
{
    options.Conventions.AuthorizeFolder("/Admin");
    options.Conventions.AuthorizeFolder("/Admin/Users", "Admins");
});

The Admin/ Index page has a Users button which we can hide from non admin users.

Edit Admin/ Index.cshtml:
@if (User.Identity.IsAuthenticated && User.IsInRole("Admin"))
{
    <a class="btn btn-primary" asp-page="/Admin/Users/Index">Users</a>
}

I add the IsAdmin property to the Admin/ Users/ Index page.

Edit Admin/ Users/ Index.cshtml:
<td>
    <div class="custom-control custom-checkbox custom-control-inline">
        <input type="checkbox" class="custom-control-input disabled" asp-for="@item.IsAdmin">
        <label class="custom-control-label disabled">
            <span class="sr-only">Never</span>
        </label>
    </div>
</td>

You can add a NameIdentifier claim to the Claims Principle at login to prevent the current user from deleting themself.

Edit Account/ Login.cshtml.cs > OnPost:
var claims = new List<Claim>
{
    new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), ClaimValueTypes.Integer),
    new Claim(ClaimTypes.Name, user.LoginName)
};
Edit Admin/ Users /Index.cshtml.cs:
public int CurrentUserId { get; set; }

public async Task OnGetAsync()
{
    var idClaim = User.FindFirst(ClaimTypes.NameIdentifier);            
    if (idClaim != null)
    {
        try
        {
            CurrentUserId = Int32.Parse(idClaim.Value);
        }
        catch (FormatException e)
        {
            Console.WriteLine(e.Message);
        }
    }
    AppUsers = await _context.AppUsers.ToListAsync();
}
Edit Admin/ Users /Index.cshtml:
<a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.Id">Details</a>
@if (Model.CurrentUserId != item.Id)
{

    <text> | </text>
    <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
}

Build, run and test. You will have to reset the Administrator user in Services/ EnsureAdministrator.cs.

Users Index page
Update 02/23/2021

I added the Enhanced User Series' article links.

Comment Count = 0

Please log in to comment.

Login Register
Logged in users receive web notifications.
Web Notifications