ASP.NET Core 3.1 - User Management
This article will demonstrate the implementation of user management. 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
Users can not self-register. We need an interface to create, read, update and delete users. We can scaffold Razor Pages with Entity Framework(CRUD) for the AppUser entity. Create a new folder under Admin named Users. I will create an Admin role in the next article which will restrict access to the Users folder. Right click on the new folder then Add then click New Scaffolded Item.
Select AppUser for the Model class and ApplicationDbContext for the Data context class. Leave the layout page option empty because it is set by _ViewStart.cshtml.
When complete, there will be Create, Delete, Details, Edit and Index pages in the Users folder. Add a link from the Admin Index page to the Users Index page.
<a class="btn btn-primary" asp-page="/Admin/Users/Index">Users</a>
The Users Index page displays a list of all users including the user's LoginNameUppercase and PasswordHash. Remove the LoginNameUppercase and PasswordHash properties and add style to the page.
To create a new user we need a login name and password. I use an InputModel decorated with DataAttributes. If the model validates, check for an exisiting login name, hash the password then save the user.
Edit Admin/ Users/ Create.cshtml.cs:
public class CreateModel : PageModel { private readonly ApplicationDbContext _context; public CreateModel(ApplicationDbContext context) { _context = context; } [BindProperty] public InputModel Input { get; set; } public class InputModel { [Required] [StringLength(32, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] [Display(Name = "Login Name")] public string LoginName { get; set; } [Required] [StringLength(32, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 8)] public string Password { get; set; } } public IActionResult OnGet() { return Page(); } public async Task<IActionResult> OnPostAsync() { if (!ModelState.IsValid) { return Page(); } // Test for an existing user with the same LoginName int testId = await _context.AppUsers .Where(u => u.LoginNameUppercase == Input.LoginName.ToUpper()) .Select(u => u.Id) .FirstOrDefaultAsync(); if (testId == 0) { var passwordHasher = new CustomPasswordHasher(); var hashedPassword = passwordHasher.HashPassword(Input.Password); var newUser = new AppUser { LoginName = Input.LoginName, LoginNameUppercase = Input.LoginName.ToUpper(), PasswordHash = hashedPassword }; _context.AppUsers.Add(newUser); try { _context.SaveChanges(); } catch (DbUpdateException) { throw new InvalidOperationException("DbUpdateException occurred creating a new user."); } } else { ModelState.AddModelError(string.Empty, "Login Name already exisits."); return Page(); } return RedirectToPage("./Index"); } }
Update the page inputs and add a little style.
The edit InputModel includes the Id and does not require a password. The password hash is not updated if the InputModel password is empty. If a password is entered, it will be validated. If the model validates, check updating the login name, hash the password if updated then save the user.
Edit Admin/ Users/ Edit.cshtml.cs:
public class EditModel : PageModel { private readonly ApplicationDbContext _context; public EditModel(ApplicationDbContext context) { _context = context; } [BindProperty] public InputModel Input { get; set; } public class InputModel { [Key] public int Id { get; set; } [Required] [StringLength(32, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] [Display(Name = "Login Name")] public string LoginName { get; set; } [StringLength(32, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 8)] public string Password { get; set; } } public async Task<IActionResult> OnGetAsync(int? id) { if (id == null) { return NotFound(); } var user = await _context.AppUsers .AsNoTracking() .FirstOrDefaultAsync(m => m.Id == id); if (user == null) { return NotFound(); } Input = new InputModel { Id = user.Id, LoginName = user.LoginName }; return Page(); } public async Task<IActionResult> OnPostAsync() { if (!ModelState.IsValid) { return Page(); } var user = await _context.AppUsers.FirstOrDefaultAsync(m => m.Id == Input.Id); if (user == null) { return NotFound(); } if (user.LoginNameUppercase != Input.LoginName.ToUpper()) { // Test for an existing user with the new LoginName int testId = await _context.AppUsers .Where(u => u.LoginNameUppercase == Input.LoginName.ToUpper()) .Select(u => u.Id) .FirstOrDefaultAsync(); if (testId != 0) { ModelState.AddModelError(string.Empty, "Login Name already exisits."); return Page(); } } // Hash new password if provided if (!string.IsNullOrEmpty(Input.Password)) { var passwordHasher = new CustomPasswordHasher(); var hashedPassword = passwordHasher.HashPassword(Input.Password); user.PasswordHash = hashedPassword; } user.LoginName = Input.LoginName; user.LoginNameUppercase = Input.LoginName.ToUpper(); _context.Attach(user).State = EntityState.Modified; try { await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!AppUserExists(user.Id)) { return NotFound(); } else { throw; } } return RedirectToPage("./Index"); } private bool AppUserExists(int id) { return _context.AppUsers.Any(e => e.Id == id); } }
Update the page inputs and add a little style.
The code for details and delete doesn't need modification but I remove the LoginNameUppercase and PasswordHash properties from the pages.
Update 02/23/2021
I added the Enhanced User Series' article links.
Comments(0)