ASP.NET Core 3.1 - Users Without Identity
This article will demonstrate the implementation of Cookie Authentication. 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. I won't use Identity or Individual User Accounts.
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 wanted a simple user management system to prototype websites for prospects. My research started with Use cookie authentication without ASP.NET Core Identity and AspNetCore.Docs/ aspnetcore/ security/ authentication/ cookie/ samples/ 3.x/ CookieSample/ . The MS Docs article and GitHub example only authenicate a hard coded user. There is a lot more to consider when you store user information in a database. This article will describe how to implement the basic Cookie Authentication. The rest of the series will present and attempt to mitigate some of the issues when you persist users to a database.
Let's start with a minimal configuration then extend and store the user class in future articles. A new razor pages project from the DotNet CLI webapp template without Identity or Individual User Accounts builds and runs without any NuGet packages installed. We will require the Microsoft. AspNetCore. Authentication. Cookies package and the Microsoft. VisualStudio. Web. CodeGeneration. Design package to scaffold new pages. Right click on the project then click Manage NuGet Packages.
Create a new root folder named Entities. Create a new class named AppUser in the Entities folder. Do not use the name User to avoid confusion and conflicts with the Claims Principle.
AppUser.cs:
public class AppUser { public string LoginName { get; set; } }
Create a new folder in Pages named Account. Create AccessDenied, Login and Logout pages in Account.
AccessDenied.cshtml:
@page @model AccessDeniedModel @{ ViewData["Title"] = "Access Denied"; } <h1>AccessDenied</h1> <h5>You do not have access to this resource.</h5>
Login.cshtml.cs:
public class LoginModel : PageModel { [BindProperty] public InputModel Input { get; set; } public string ReturnUrl { get; set; } public class InputModel { [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] [DataType(DataType.Password)] [StringLength(32, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 8)] public string Password { get; set; } [Display(Name = "Remember me?")] public bool RememberMe { get; set; } } public async Task OnGetAsync(string returnUrl = null) { // Clear the existing external cookie await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); returnUrl ??= Url.Content("~/"); ReturnUrl = returnUrl; } public async Task<IActionResult> OnPostAsync(string returnUrl = null) { ReturnUrl = returnUrl; if (ModelState.IsValid) { var user = await AuthenticateUser(Input.LoginName, Input.Password); if (user == null) { ModelState.AddModelError(string.Empty, "Invalid login attempt."); return Page(); } var claims = new List<Claim> { new Claim(ClaimTypes.Name, user.LoginName) }; var claimsIdentity=new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), new AuthenticationProperties { IsPersistent=Input.RememberMe }); if (!Url.IsLocalUrl(returnUrl)) { returnUrl=Url.Content("~/"); } return LocalRedirect(returnUrl); } // Something failed. Redisplay the form. return Page(); } private async Task<AppUser> AuthenticateUser(string login, string password) { if (string.IsNullOrEmpty(login) || string.IsNullOrEmpty(password)) { return null; } // For demonstration purposes, authenticate a user // with a static login name and password. // Assume that checking the database takes 500ms await Task.Delay(500); if (login.ToUpper() !="ADMINISTRATOR" || password !="P@ssw0rd" ) { return null; } return new AppUser() { LoginName="Administrator" }; } }
Login.cshtml:
@page @model LoginModel @{ ViewData["Title"] = "Login"; } <h1>Login</h1> <div class="row"> <div class="col-md-3 col-lg-4"></div> <div class="col-md-6 col-lg-4"> <form method="post"> <div asp-validation-summary="All" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.LoginName"></label> <input asp-for="Input.LoginName" class="form-control"> <span asp-validation-for="Input.LoginName" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Input.Password"></label> <input asp-for="Input.Password" class="form-control"> <span asp-validation-for="Input.Password" class="text-danger"></span> </div> <div class="form-group"> <div class="custom-control custom-checkbox"> <input class="custom-control-input" asp-for="Input.RememberMe" /> <label class="custom-control-label" asp-for="Input.RememberMe"> @Html.DisplayNameFor(model => model.Input.RememberMe) </label> </div> <span asp-validation-for="Input.RememberMe" class="text-danger"></span> </div> <div class="form-group"> <button type="submit" class="btn btn-primary">Log in</button> </div> </form> </div> <div class="col-md-3 col-lg-4"></div> </div> @section Scripts { <partial name="_ValidationScriptsPartial" /> }
Logout.cshtml.cs:
public class LogoutModel : PageModel { public IActionResult OnGet() { if (User.Identity.IsAuthenticated) { // Redirect to home page if the user is authenticated. return RedirectToPage("/Index"); } return Page(); } public async Task<IActionResult> OnPostAsync() { await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return RedirectToPage("/Account/Logout"); } }
Logout.cshtml:
@page @model LogoutModel @{ ViewData["Title"] = "Logged Out"; } <h1>Logged Out</h1> <h5>You have successfully signed out.</h5>
Create _LoginPartial.cshtml in Pages/Shared.
_LoginPartial.cshtml:
<ul class="navbar-nav"> @if (User.Identity.IsAuthenticated) { <li class="nav-item"> <form class="form-inline" asp-page="/Account/Logout" method="post"> <button type="submit" class="nav-link text-dark btn btn-link">Logout @User.Identity.Name</button> </form> </li> } else { <li class="nav-item"> <a class="nav-link text-dark" asp-page="/Account/Login">Log In</a> </li> } </ul>
Add an Admin folder to test Authentication. Create a new folder in Pages named Admin. Create an Index page in Admin.
Index.cshtml:
@page @model IndexModel @{ ViewData["Title"] = "Admin"; } <h1>Admin</h1> <h5>You must be signed in to access this resource.</h5>
Edit _Layout.cshtml, add _LoginPartial to the Navbar.
_Layout.cshtml:
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse"> <partial name="_LoginPartial" /> <ul class="navbar-nav flex-grow-1">
Edit _Layout.cshtml, add an Admin nav-link to the Navbar.
_Layout.cshtml:
<li class="nav-item"> <a class="nav-link text-dark" asp-page="/admin/index">Admin</a> </li>
Update Namespaces and resolve all using statements. You should be able to build without errors. Although the project will run, an attempt to Login results with an error. InvalidOperationException: No sign-out authentication handlers are registered. Did you forget to call AddAuthentication(). AddCookies("Cookies",...)?
AddAuthentication(). AddCookies("Cookies",...) is implemented in Startup.cs > ConfigureServices. We need to add the AuthorizeFolder to the services. AddRazorPages() options.
Edit Startup.cs > ConfigureServices:
public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(options => { options.Conventions.AuthorizeFolder("/Admin"); }); services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(); }
Startup.cs > Configure needs app. UseAuthentication() and we should to add the app. UseCookiePolicy().
Edit Startup.cs > Configure:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseCookiePolicy(new CookiePolicyOptions() { MinimumSameSitePolicy = SameSiteMode.Strict }); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); }); }
Build, run and test. Navigation to Admin will redirect to Login for non authenticated users. Login with Login Name = Administrator and Password = P@ssw0rd.
Update 02/23/2021
I added the Enhanced User Series' article links.
Comments(0)