ASP.NET Core 3.1 - Users Without Identity


Ken Haggerty
Created 01/23/2020 - Updated 02/02/2020 16:20

This article will demonstrate the implementation of Cookie Authentication. I will assume you have created a new ASP.NET Core 3.1 Razor Pages project. I won't use Identity or Individual User Accounts. See Tutorial: Get started with Razor Pages in ASP.NET Core

This article is part of a series about users without Identity.


Access to the research project source code may be purchased on KenHaggerty.Com at Manage > Assets.

A project which implements users without Identity has been published to demo.kenhaggerty.com.

I enjoy writing these articles. It often enhances and clarifies my coding. The research project is a result of a lot of refactoring and hopefully provides logical segues for the articles. Thank you for supporting my efforts.

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

Article Tags:

Claims Model Scaffold

1 Following


Comment Count = 0

Please log in to comment or follow.

Login Register