Authentication in ASP .NET Core

Chris Noring - Feb 1 '22 - - Dev Community

This article covers authentication in ASP .NET Core. It tries to explain the concepts and how they relate and also shows some code so you can hopefully add authentication to your own .NET app.

Authenticating a user means determining a user's identity. We do this to ensure they are who they say they are. Once we ensure we trust them, we can log them into our app and show them resources that only logged in users should have access to.

Main scenarios

Here's the main scenarios we're looking to address:

  • Authenticating a user.
  • Responding when an unauthenticated user tries to access a restricted resource.

Handlers, services to handle the authentication flows

Setting up authentication

To carry out an authentication flow, you need a handler. You can have more than one handler, but it must implement the IAuthenticationService interface. The handler/s is used by the authentication middleware. The registered authentication handlers and their configuration options are called "schemes".

A scheme

To use a scheme, you need to register it here Startup.ConfigureServices, i.e. like so:



void ConfigureServices() 
{
  // register your scheme
}


Enter fullscreen mode Exit fullscreen mode

What you need to do is to first call AddAuthentication() followed by a call to a specific scheme, like the below example:



void ConfigureServices() 
{
  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>   Configuration.Bind("JwtSettings", options))
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => Configuration.Bind("CookieSettings", options));
}


Enter fullscreen mode Exit fullscreen mode

Here we call AddAuthentication() followed by a call to both the schemes AddJwtBearer() and AddCookie(). By calling these schemes, you register them and their config settings.

You don't always need to explicitly call AddAuthentication(), if you use ASP .NET Identity, that call is done internally for you.

Set up middleware

To understand why we do this step, lets first talk about the request pipeline. Imagine the following scenario:

The calling client makes a request towards a resource on your system. To allow for that to happen, they need to be logged in to your system. To log in, the user needs to provide credentials (typically username and password) that convinces us they are who they say they are. We call this to authenticate. However, we need to write code to ensure the request is intercepted and we have a chance to validate the user. This is why we now need to talk to middleware and configure it to use the handlers/schemes we registered thus far with ASP .NET.

Now we go to another method Configure() in our Startup class and call UseAuthentication() like so:



void Configure() 
{
  UseAuthentication();
}


Enter fullscreen mode Exit fullscreen mode

You need to call UseAuthentication() in the right time so anything dependent on the auth can use it. Here's som guidelines:

  • After UseRouting(), so that route information is available for authentication decisions.
  • Before UseEndpoints(), so that users are authenticated before accessing the endpoints.

Authenticating the user

Authenticating the user

Ok, so you've seen two parts of three so far:

  1. Register scheme/handler
  2. Instructing the pipeline to use it
  3. Authenticating the user <- to be explained

Let's discuss how to authenticate the user. We will do that by borrowing some code from a sample project that does Cookie authentication Cookie authentication

Register scheme/handler

First, let's check 1) the registering of the scheme, in ConfigureServices() method in Startup.cs:



public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();

        services.AddAuthentication(CookieScheme) // Sets the default scheme to cookies
            .AddCookie(CookieScheme, options =>
            {
                options.AccessDeniedPath = "/account/denied";
                options.LoginPath = "/account/login";
            });

        // Example of how to customize a particular instance of cookie options and
        // is able to also use other services.
        services.AddSingleton<IConfigureOptions<CookieAuthenticationOptions>, ConfigureMyCookie>();
    }


Enter fullscreen mode Exit fullscreen mode

Note the call to AddCookie() for registering the scheme and the config AccessDeniedPath and LoginPath. By specifying those config values, we know what route to send the user to if they are:

  • unable to provide credentials /account/denied
  • a place to provide credentials and be logged in /account/login

Configure middleware

We've registered the scheme and is ready to use it. Our next step is explicitly telling our app to use it by a call to UseAuthentication().



public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}


Enter fullscreen mode Exit fullscreen mode

Note the call to app.UseAuthentication() and how it's after the call to UseRouting(), so we have the routing information, and before the call to UseEndpoints() so the latter can leverage the authentication capability.

Doing the actual authentication

When we registered the scheme, we told it where to go for logins, i.e /account/login, that's a controller class and method we need to write. So, create a AccountController.cs with the following content:



using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;

namespace AuthSamples.Cookies.Controllers;

public class AccountController : Controller
{
}


Enter fullscreen mode Exit fullscreen mode

What we need to fill this class with is the following:

  • a way to render a login form
  • a method that handles a user sending their login credentials and have those validated
  • a method for handling logging out

Rendering a login form

Here we our controller code becomes simple, like so:



[HttpGet]
public IActionResult Login(string returnUrl = null)
{
    ViewData["ReturnUrl"] = returnUrl;
    return View();
}


Enter fullscreen mode Exit fullscreen mode

It becomes simple as there's a template that we render Login.cshtml:



<h2>Login</h2>

<div class="row">
    <div class="col-md-8">
        <section>
            <form asp-controller="Account" asp-action="Login" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal" role="form">
                <div class="form-group">
                    <label class="col-md-2 control-label">User</label>
                    <div class="col-md-10">
                        <input type="text" name="username" />
                    </div>
                </div>

                <div class="form-group">
                    <label class="col-md-2 control-label">Password</label>
                    <div class="col-md-10">
                        <input type="password" name="password" />
                    </div>
                </div>

                <div class="form-group">
                    <div class="col-md-offset-2 col-md-10">
                        <button type="submit" class="btn btn-default">Log in</button>
                    </div>
                </div>
            </form>
        </section>
    </div>
</div>


Enter fullscreen mode Exit fullscreen mode

The above template consists of a username field, a password field and submit button.

Handling login request

Imagine now the user enters their credentials in the above form, then we need controller code to handle that:



private bool ValidateLogin(string userName, string password)
{
    // For this sample, all logins are successful.
    return true;
}

[HttpPost]
public async Task<IActionResult> Login(string userName, string password, string returnUrl = null)
{
    ViewData["ReturnUrl"] = returnUrl;

    // Normally Identity handles sign in, but you can do it directly
    if (ValidateLogin(userName, password))
    {
        var claims = new List<Claim>
            {
                new Claim("user", userName),
                new Claim("role", "Member")
            };

        await HttpContext.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity(claims, "Cookies", "user", "role")));

        if (Url.IsLocalUrl(returnUrl))
        {
            return Redirect(returnUrl);
        }
        else
        {
            return Redirect("/");
        }
    }

    return View();
}


Enter fullscreen mode Exit fullscreen mode
  • the Login() method being called via a POST request when the user hits the login button. We are provided with username and password and we call ValidateLogin() (here you typically want a call to a database to validate the user, ensure they exist, the password is correct etc).
  • Then we sign in the user, if ValidateLogin() responds with true.
  • The sign in happens when we call HttpContext.SignInAsync(). Said method needs a ClaimsPrincipal instance to be created. That object needs a ClaimsIdentity object as an instance. High-level we say who the user is and can later determine what parts of the system the user shall have access to, in an authorization process.

Handle logout request

The user might want to logout, to handle that, you can add another method to your AccountController class like so:



public async Task<IActionResult> Logout()
{
    await HttpContext.SignOutAsync();
    return Redirect("/");
}


Enter fullscreen mode Exit fullscreen mode

Calling HttpContext.SignOutAsync() will ensure that the app forgets all about you and your login session.

What goes on here is that:

Routing the user correctly

At this point you are wondering, how does the system know how to send the users to this login form, like how do I check who's logged in or not?

This is when we have a look at the HomeController.cs file. It uses an attribute class Authorize to force the user to the login page if they are not authenticated. Here's how that would work:

  1. User tries to go to route /Home/MyClaims


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


Enter fullscreen mode Exit fullscreen mode
  1. If the user is logged in, then they are presented with the view represented by MyClaims function. If not logged in, then they are taken to account/login.

Summary

Hopefully, you now have a good starting point for understanding how to authenticate and login a user to an ASP .NET application. In the next part we will look at how you can authorize a user, i.e determine what user should have access to what resources.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .