K Cartlidge

C#/DotNet. Go. Node. Python/Flask. Elixir.

DotNet Core Authentication 1 - Sites

(Updated from the original version of 8th March 2020)

Sometimes the built-in options for authentication (Individual User Accounts, Work or School Accounts, or Windows Authentication) are not what you want. Most small sites will go for the first option, but as standard you usually end up with Entity Framework, EF migrations, and SQL Server (Express etc).

Whilst you can override, delete, or edit all this, it's much easier to never generate it to start with. Especially if you intend to use an alternative database such as Postgres. What follows is a set of extremely simple steps to get the basics in place without pre-generation.

You'll get [Authorize] attributes for controller/route protection, supported by cookie-based user sessions. It doesn't go as far as creating the database, but it gets you to the point where you just need to add your own logic to a single Login method. You will have Login, Logout, and a protected (stub) Dashboard.

There is a Github repository which contains all the below steps as a sequence of commits.

Create the solution

Update: This continues to work with DotNet 7. The only difference is the lack of a Startup.cs file which involves a trivial change noted at the appropriate point below.

Using Visual Studio 2019 v16.5 Preview or later, create as follows:

Update the configuration

The first thing is to register the use of cookies for maintaining authentication state. This is done during startup.

Startup.cs

Update: In later versions of DotNet this file has gone and we edit Program.cs instead. The code is the same but is now placed in the Main method, and services is replaced with builder.Services.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authentication.Cookies;

public void ConfigureServices(IServiceCollection services)
{
    // Add just before AddControllersWithViews()
    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(o =>
    {
        o.Cookie.HttpOnly = true;
        o.Cookie.SameSite = SameSiteMode.Strict;
        o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        o.ExpireTimeSpan = TimeSpan.FromMinutes(120);
        o.SlidingExpiration = true;
    });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // Add just before UseHttpsRedirection()
    app.UseAuthentication();
}

Add a sample protected route

With that in place you need a sample endpoint to test/demo it. One simple way is to add a Dashboard to the HomeController (which already exists).

HomeController.cs

using Microsoft.AspNetCore.Authorization;

[Authorize()]
public IActionResult Dashboard()
{
    return View();
}

Test accessing that route

Navigate to Home/Dashboard in your site. You should get a 404 but if you look carefully you'll see it isn't the dashboard that can't be found. It's an Account/Login page.

This is because the Authorize attribute has protected the Home/Dashboard route by redirecting you away to the default login screen (which you don't yet have).

Add the ability to login

There is a default Account controller expected by DotNet Core, that is also expected to have a Login action. In the real world GET AccountController.Login() would request the user's credentials. POST AccountController.Login() would receive the answers, check them, then sign the user in. For this brief summary we will just sign them in automatically without them needing to provide their credentials.

Add a new Controller using the template for MVC Controller - Empty, and call it AccountController. You'll get a default Index action, which you should rename to Login and whose contents should be removed and replaced with what follows.

AccountController.cs

using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;

public async Task<IActionResult> Login()
{
    var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
    identity.AddClaim(new Claim(ClaimTypes.Name, "Example"));
    identity.AddClaim(new Claim("ID", "1"));
    identity.AddClaim(new Claim("IsAdmin", "Y"));
    var principal = new ClaimsPrincipal(identity);
    await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
    return RedirectToAction("Dashboard", "Home");
}

This code will log you in without requiring credentials. It does this by creating a set of claims in an Identity, which is then used to sign you in.

Add the ability to logout

Clearing cookies should work. Traditionally however we would expect a simple link or button that would log users out of the site. The following does that, removing the cookie as it goes.

AccountController

public async Task<IActionResult> Logout()
{
    await HttpContext.SignOutAsync();
    return RedirectToAction("Index", "Home");
}

Updating layouts and views

There is a standard object available to both the layout template and any controller actions. It's called User, and from it we always have access to any logged in user.

First let's update the main layout file to work out the login details. Insert the following on the line below the !DOCTYPE declaration.

_Layout.cshtml

This creates a view-level pair of helpers.

@{
    var loggedIn = (User != null && User.Identity.IsAuthenticated);
    var name = (loggedIn ? User.Identity.Name : string.Empty);
}

We need to insert a new block between the LI tags for Home and Privacy. This adds menu options which are dynamic according to your login status.

@if (loggedIn)
{
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Dashboard">Dashboard</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="" asp-controller="Account" asp-action="Logout">Logout</a>
    </li>
}
else
{
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="" asp-controller="Account" asp-action="Login">Login</a>
    </li>
}

Now just above the @RenderBody() (inside main) add this. It will show a strip below the menu with your login status.

@if (loggedIn)
{
    <p>Logged in as <strong>@name</strong></p>
}

You can use the same loggedIn flag anywhere in the view, and even place it in the AccountController to get access during request handling.

Final and next steps

What you've got

If you run the site now you should be able to login and logout, seeing the menu change as needed. You'll also get a (stub) dashboard page and a User instance available in both controllers and views, to which you can attach any info you're happy being transmitted in cookies to avoid database trips.

Next steps