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.