Cookie authentication with social providers in ASP.NET Core

When needing to implement authentication in ASP.NET Core, there are several different options. Almost all of the documentation and examples expect you to use ASP.NET Core Identify. For a SQL Server-based application, Identity may be a good choice. But a lot of applications don't need the rich feature set available with Identity. In this post, I'll show you how to implement cookie-based authentication with support for social providers in ASP.NET Core.

When migrating our main application to ASP.NET Core, we had to change our authentication code. The old application was based on SimpleAuthentication, which was the coolest authentication framework 6 years ago. During the years, ASP.NET moved from membership provider to Identity and ASP.NET Core was released with an authentication solution. We looked at a range of different options for migrating the authentication code and settled on the packages provided by Microsoft.

Before I start digging into the code, let's put a few words on ASP.NET Core Identity. Identity is a full-blown authentication and authorization feature available for ASP.NET Core. It contains a lot of features like storing users in a database, scaffolding Razor pages, and much more. It might be a good solution if you are building an ASP.NET Core website with EntityFramework and a lot of the other features that come out of the box with core. Make sure to check out Identity to choose whether you want to use that or the more manual approach presented in this post.

For this post, I'll implement authentication using an authentication cookie. When the user signs in using a social provider, we will define a callback that sets the same cookie, making it consistent across sign-in methods. Start by adding cookie-based authentication in the ConfigureServices method in the Startup.cs file:

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services
        .AddAuthentication(options =>
        {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        })
        .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
        {
            options.LoginPath = "/home/login";
            options.LogoutPath = "/home/logout";
        });

I'm calling the AddAuthentication method and provide the cookie authentication scheme as the default. By calling the AddCookie method, I have access to the cookie options which let me set custom URLs for login and logout pages.

Call the UseAuthentication and UseAuthorization methods in the Configure method:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ...
    app.UseAuthentication();
    app.UseAuthorization();
    // ...

The order in which these two methods are called is important. Make sure to call them after setting up exception handling middleware but before calls to UseMvc, UseEndpoints, etc.

For test, I'm creating a simple login form inside a new file named Views\Home\Login.cshtml:

<h1>Login</h1>

<form  method="post">
    Username:
    <input name="username" />
    Password:
    <input name="password" type="password" />
    <button asp-controller="Home" asp-action="Login">Login</button>
</form>```

Ok, this probably won't win any design award. But it let us test login. Add the action to the HomeController:

public IActionResult Login()
{
    return View();
}

To handle the post from the form, add a new Login method to HomeController:

[HttpPost]
public async Task<IActionResult> Login(string username, string password)
{
    // Validate username and password

    List<Claim> claims = new List<Claim>();
    claims.Add(new Claim(ClaimTypes.NameIdentifier, username));
    ClaimsIdentity identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
    ClaimsPrincipal principal = new ClaimsPrincipal(identity);
    await HttpContext.SignInAsync(principal);
                
    return Redirect("/home/profile");
}

Let's go through the lines one by one. The first thing you will need to do is to validate the posted username and password. How you want to implement this, depends on where and how you have your users stored. Probably in a database somewhere. If the username and/or password doesn't validate, you will need to return an error response to the client and not run code after the comment.

Next, we need to call the SignInAsync method on HttpContext. The method needs a ClaimsPrincipal which again needs a ClaimsIdentity. The identity contains a list of claims which can be the id of the user, the name, the email, etc. In this example, we put the user's username in the identity.

When successfully signed in, we redirect to a profile page that I have created an action for in HomeController and a simple view:

<h1>Profile</h1>

<a href="/home/logout">Log out</a>

To implement sign out, add a Logout action to HomeController:

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

SignOutAsync invalidates the authentication cookie and redirects to the front page.

The code so far implements login with username and password. To add a social provider, I'll use the Microsoft provider as an example. Follow the steps in the official documentation to create a new app. Then install the Microsoft provider:

Install-Package Microsoft.AspNetCore.Authentication.MicrosoftAccount

There are packages for all major OAuth providers. To redirect the user to the login screen on Microsoft, I'm adding a simple GET request on my HomeController:

public ActionResult LoginMicrosoft(string returnUrl)
{
    return Challenge(MicrosoftAccountDefaults.AuthenticationScheme);
}

By returning the result of Challenge with Microsoft account as the authentication scheme, the browser is redirected to the login screen on Microsoft. On the login page, I'm adding a link to /home/loginmicrosoft to let the user sign in with Microsoft.

Once the user successfully signs in on Microsoft your application receives a request on /signin-microsoft. This endpoint is consumed by ASP.NET Core and not something that you will need to implement. To enable Microsoft sign in and to be notified when successfully signed in, set up the Microsoft provider in Startup.cs:

services
    .AddAuthentication(...)
    .AddCookie(...)
    .AddMicrosoftAccount(options =>
    {
        options.ClientId = "ClientId";
        options.ClientSecret = "ClientSecret";
        options.Events = new OAuthEvents
        {
            OnTicketReceived = ctx =>
            {
                var username = ctx.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
                if (string.IsNullOrWhiteSpace(username))
                {
                    ctx.HandleResponse();
                    ctx.Response.Redirect("/");
                    return Task.CompletedTask;
                }

                if (!UserExists(username))
                {
                    CreateUser(username);
                }

                return Task.CompletedTask;
            }
        };
    });

We have already gone through the code for AddAuthentication and AddCookie why I have left that out. The new part is calling AddMicrosoftAccount and providing it with a set of options. The ClientId and ClientSecret properties are common for all social providers and the values are available on the Microsoft app.

By calling AddMicrosoftAccount the provider will automatically call the SignInAsync method when successfully authorizing a new Microsoft account (inside the /signin-microsoft request as already discussed). To overwrite if and when a Microsoft account should be allowed to sign in or not, we implement the OnTicketReceived function. In there, we can disallow the sign in by calling ctx.HandleResponse() and returning from the method. By calling HandleResponse we tell ASP.NET Core that it shouldn't run the default code when authenticating with the social provider.

In case we received a username from the social provider we validate that it exists in the database. If not we create a new user in the database. Like before, how you want to implement these methods depends on how and where you store your users. Finally, we return from the method and let the social provider sign in using the configured authentication scheme (cookie).

That's what it takes to implement authentication with both username/password and social providers without ASP.NET Core Identity. The official documentation covers each scenario individually, but I think that a well-combined post was missing, especially around the OnTicketReceived logic. Hopefully, this post will help you not to get stuck in the same places I did.