Creating a simple login in ASP.NET Core 2 using Authentication and Authorization (NOT Identity)

03 November 2017

Sometimes you just need a really simple login system for an application; something as simple as a single fixed username and password. Obvioulsy this wouldn't be great for a fully featured application, but it could get a prototype project up-and-running fast without needing to create a full SQL Server database for ASP.NET Core Identity to use.

This article decribes a really minimal login/logout implemenataion using ASP.NET Core 2 Authentication and Authorization; it was inspired by this Cookie authentication in ASP.NET Core 2 without ASP.NET Identity article but with a fully implemented project written using Razor Pages in the release version of ASP.NET Core 2.

You can download the source code here.


Configuring the application

I have kept as much configuration as possible in the Startup.cs class and removed anything not essesntial, the code looks like ths:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
    {
        options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    }).AddCookie(options => { options.LoginPath = "/Login"; });
    services.AddMvc().AddRazorPagesOptions(options =>
    {
        options.Conventions.AuthorizeFolder("/");
        options.Conventions.AllowAnonymousToPage("/Login");
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseAuthentication();
    app.UseMvc();
}

The code in ConfigureServices is adding support for authentication using cookies, telling authentication that the login page has the URL /Login and that this can be access anonymously (i.e. without being logged in) whereas all other files in the root require authentication to be accessed.

The Configure method then tells our site to use Authentication and MVC (so that we can create some pages).

Creating the pages

The next step is to add an Index (home) page, which will only be accessed when logged-in due to our settings in ConfigureServices, and a Login page to give us access to the system.

My Index page was added as a Razor Page without a model, it is super simple (I will add slightly to this later in the article):

@page

<!DOCTYPE html>

<html>
<body>
    <h1>Welcome to your login-protected area!</h1>
</body>
</html>

My Login page was created as a Razor Page with a model, it is a very simple form:

@page
@model SimpleLogin.Pages.LoginModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<!DOCTYPE html>

<html>
<body>
    <p>Login using form below:</p>
    <form method="post">
        <div asp-validation-summary="All" class="text-danger"></div>
        Username: <input asp-for="loginData.Username" value="username" /><br/>
        Password: <input asp-for="loginData.Password" value="password" /><br />
        Remember me: <input asp-for="loginData.RememberMe" type="checkbox" /><br />
        <input type="submit" value="Login" />
        @Html.AntiForgeryToken()
    </form>
</body>
</html>

The C# model code for the Login page is as follows (I have ommited the using and namespace lines for brevity, but thats all in the downloadable project):

public class LoginModel : PageModel
{
    [BindProperty]
    public LoginData loginData { get; set; }

    public async Task OnPostAsync()
    {
        if (ModelState.IsValid)
        {
            var isValid = (loginData.Username == "username" && loginData.Password == "password");
            if (!isValid)
            {
                ModelState.AddModelError("", "username or password is invalid");
                return Page();
            }
            var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme, ClaimTypes.Name, ClaimTypes.Role);
            identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, loginData.Username));
            identity.AddClaim(new Claim(ClaimTypes.Name, loginData.Username));
            var principal = new ClaimsPrincipal(identity);
            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, new AuthenticationProperties { IsPersistent = loginData.RememberMe });
            return RedirectToPage("Index");
        }
        else
        {
            ModelState.AddModelError("", "username or password is blank");
            return Page();
        }
    }

    public class LoginData
    {
        [Required]
        public string Username { get; set; }

        [Required, DataType(DataType.Password)]
        public string Password { get; set; }

        public bool RememberMe { get; set; }
    }
}

This uses standard Razor Page techniques to bind the LoginData upon post-back. The OnPostAsync method handles validation of the posted data and checks the username and password match the hardcoded values (these could perhaps come from a config file to allow easier amendment). If everything is good then it runs some stock code to create a ClaimsIdentity and ClaimsPrincipal to perform the actual login using SignInAsync (you don't particularly need to know how this stock code works but there is plenty of reference online if you want to reserach further).

Summing up

That's basically it, you can now build and run this project (you will need the ASP.NET Core 2 Runtime or SDK installed). If you get a blank screen then there is an error - add a web.config file to the project and uncomment the contents, next set the logging attribute to True and create a folder named logs in the root folder of your project; run the app and view the resulting log file to find your error.

My final addition to this project was to place a Logout button on the Index page, I decided to do this with an in-page function rather than code-behind (to show that both options are possible):

@page

@using Microsoft.AspNetCore.Authentication

@functions {
    public async Task<IActionResult> OnPostAsync()
    {
        await HttpContext.SignOutAsync();
        return RedirectToPage("Login");
    }
}

<!DOCTYPE html>

<html>
<body>
    <h1>Welcome to your login-protected area!</h1>
    <form method="post">
        <input type="submit" value="Logout" />
        @Html.AntiForgeryToken()
    </form>
</body>
</html>

All this code is available in the project download so feel free to take a look.