ASP.NET Authentication Explained

Explore ASP.NET Core authentication and learn about login processes, MFA, biometrics, and workflows to secure your ASP.NET Core app.

Paul Williams |

Web applications written in ASP.NET Core require a strong cybersecurity footprint. This is especially true for complex apps that utilize a range of APIs and associated resources, such as media. In nearly all cases, these applications require users to be authenticated by using a traditional login process. Entering a name and password remains common, sometimes combined with Multi-Factor Authentication (MFA) or biometrics.

Let's examine the authentication process in ASP.NET Core applications. We will cover various workflows used for authentication, including for authenticated users and those seeking unauthorized access. Note that ASP.NET Core includes a built-in service called ASP.NET Core Identity, that supports the entire authentication process.

Additionally, Duende IdentityServer integrates seamlessly with many ASP.NET authentication strategies. It provides a full security framework for ASP.NET Core, supporting modern standards such as OAuth 2.0 and OpenID Connect (OIDC). It's the right approach for securing any web application on the ASP.NET Core platform.

Defining Authentication Flows in ASP.NET Core

An authentication handler is needed when building an authentication flow in an ASP.NET Core application. The first step in adding authentication to an app involves registering the schemes and handlers used for this process.

Typically, handlers are registered in an ASP.NET Core application's startup code by registering them as services. Call the AddAuthentication function, followed by specific calls to each authentication scheme. Check out the following code snippet for an example:

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

Note that when using Microsoft's ASP.NET Identity framework, the original call to the AddAuthentication function is handled automatically.

Now, the application's middleware needs to be configured to use authentication and the handlers registered earlier. In the application's startup code, a call to the UseAuthentication function is placed between calls to the UseRouting and UseEndpoints functions. This placement ensures the routing information is available to the authentication process. Additionally, it ensures a user is properly authenticated before accessing the application's endpoints.

Authenticating the User in ASP.NET Core Applications

After registering the authentication schemes and instructing the request pipeline to use them, let's actually authenticate the user. The first step in this process involves registering the authentication scheme. In the following example, the cookie method of authentication is used. Take a closer look at this short code snippet:

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

Of particular importance are the two paths defined in the options object. These two path-related variables in the options object are set to two different URLs. The AccessDeniedPath is referenced when an unauthorized user attempts to access the application. If the user isn't actually logged in, they should be redirected to the URL referenced in the LoginPath variable. This allows the user to log in before attempting to use the application.

With the authentication scheme registered, now add code to the application's middleware to utilize it. As noted earlier, the call to UseAuthentication must be placed between the calls to UseRouting and UseEndpoints. This ensures the authentication process has routing information, while making sure the app's endpoints require an authenticated user.

Adding Functionality to the Login Form

Now it's time to flesh out the functionality for the login page referenced earlier in the LoginPath variable. Once again, the user is redirected to this page when trying to use the application without first being authenticated. In addition to a basic page design, it needs methods for users to log in and out of the application.

This simple form includes text boxes for a user ID and password, as well as a submit button. There is also a logout button. Check out this sample code from the login method used by this form. It utilizes Microsoft's ASP.NET Core MVC library.

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, CookieAuthenticationDefaults.AuthenticationScheme, "user", "role")));

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

    return View();
}

This login method is called when the user submits their credentials when clicking on the form's submit button. Note that in this sample code, the ValidateLogin method always returns true. Examine how the new claims object embeds the user's name and their role as key-value pairs. A new ClaimsPrincipal object instantiates a new ClaimsIdentity object using the claims object as the argument, along with the cookies authorization scheme noted earlier (CookieAuthenticationDefaults.AuthenticationScheme).

The asynchronous SignInAsync function is now called with that instantiated ClaimsPrincipal object. This actually performs the authentication process, redirecting the user to the denoted URL upon success. It effectively illustrates the simplicity of implementing user authentication on an ASP.NET Core application.

What About Implementing the Logout Button?

However, we also need to implement the functionality of the logout button, which is also quite straightforward. It simply involves adding a new function to the application's AccountController class.

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

Calling the SignOutAsync function effectively logs the user out of the application, redirecting to the root.

Finally, the application utilizes its Authorize attribute class in HomeController.cs to verify the user's authentication status.

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

In this example, if an unauthenticated user tries to access MyClaims, they are redirected to the login form. Once again, it illustrates how protecting an ASP.NET Core web application with authentication is a straightforward effort.

Duende IdentityServer makes ASP.NET Core Authentication more Powerful

This quick example illustrates the simplicity of ASP.NET authentication. However, if you want to support OpenID Connect and OAuth 2.0 in your ASP.NET Core applications, check out Duende IdentityServer. It provides a standards-compliant security framework for your application, while you control your own business logic and user experience. It's a state-of-the-art option for protecting your company's web applications.