ASP.NET Core 3.1 - Admin Role
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
Creation Series
- ASP.NET Core 3.1 - Users Without Identity
- ASP.NET Core 3.1 - User Entity
- ASP.NET Core 3.1 - Password Hasher
- ASP.NET Core 3.1 - User Management
- ASP.NET Core 3.1 - Admin Role
- ASP.NET Core 3.1 - Cookie Validator
- ASP.NET Core 3.1 - Concurrency Conflicts
- ASP.NET Core 3.1 - Must Change Password
- ASP.NET Core 3.1 - User Database Service
- ASP.NET Core 3.1 - Rename Related Entities
2FA Series
- ASP.NET Core 3.1 - 2FA Without Identity
- ASP.NET Core 3.1 - 2FA User Tokens
- ASP.NET Core 3.1 - 2FA Cookie Schemes
- ASP.NET Core 3.1 - 2FA Authenticating
- ASP.NET Core 3.1 - 2FA Sign In Service
- ASP.NET Core 3.1 - 2FA QR Code Generator
- ASP.NET Core 3.1 - Admin 2FA Requirement
- ASP.NET Core 3.1 - 2FA Admin Override
Enhanced User Series
- ASP.NET Core 3.1 - Enhanced User Without Identity
- ASP.NET Core 3.1 - 2FA Recovery Codes
- ASP.NET Core 3.1 - Login Lockout
- ASP.NET Core 3.1 - Created And Last Login Date
- ASP.NET Core 3.1 - Security Stamp
- ASP.NET Core 3.1 - Token Service
- ASP.NET Core 3.1 - Confirmed Email Address
- ASP.NET Core 3.1 - Password Recovery
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.
Update 02/23/2021
I added the Enhanced User Series' article links.
Comments(0)