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

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.

Installed 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.

Login Administrator.
Admin Page.
Update 02/23/2021

I added the Enhanced User Series' article links.

Ken Haggerty
Created 01/23/20
Updated 02/24/21 00:19 GMT

Log In or Reset Quota to read more.

Article Tags:

Claims Model Scaffold
Successfully completed. Thank you for contributing.
Processing...
Something went wrong. Please try again.
Contribute to enjoy content without advertisments.
You can contribute without registering.

Comments(0)

Loading...
Loading...

Not accepting new comments.

Submit your comment. Comments are moderated.

User Image.
DisplayedName - Member Since ?