Securing .NET Core 3 API with Cookie Authentication

Securing .NET Core 3 API with Cookie Authentication

In last article, we have seen how to add ASP .NET Core identity to your web API project.

In this blog post, let’s see how to setup your web API project for cookie authentication. We will authenticate the users using the data in ASP .NET Core identity tables for the demo.

Cookie Auth with Web APIs ?

Cookie authentication is recommended for interactive web applications, while JWT (aka bearer token) authentication is recommended for web APIs.

So then why are we trying to apply cookie authentication on Web APIs ? Well, in my opinion, we can understand the concept on how cookie authentication can be configured on web project. Once we know the concept, the same concept can be applied to interactive web applications as well.

Source Code To Begin

If you have followed my previous blog, you will have a Web API project with .NET Core identity already configured.

If not, you can get the base solution from my GitHub repository. We will further extend this to enable Cookie authentication.

The solution has only one project. The project already has all the configurations to setup ASP .NET Core identity.

If you run the EF migrations, it will create the database for you. Again, refer my previous blog article for step by step guide. You should be able to run the solution now.

NuGet package references

As we are using .NET Core API project, the Microsoft.AspNetCore.App metapackage is already referenced in the project.

If you are using any project which is not referencing this NuGet package, then you will be required to add the package reference to Microsoft.AspNetCore.Authentication.Cookies package.

Startup.ConfigureServices method

In Startup.ConfigureServices, you should to create authentication middleware services using AddAuthentication and AddCookie methods as shown below.

public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("SqlConnection")));
services.AddIdentity<IdentityUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.SlidingExpiration = true;
options.ExpireTimeSpan = new TimeSpan(0, 1, 0);
});
services.AddControllers();
}
view raw startup.cs hosted with ❤ by GitHub

The authentication scheme specified in the AddAuthentication is just a string (in this case it resolves to “Cookie”). You can specify any string value as Authentication Scheme to distinguish it from other schemes in case there are more than one schemes.

AuthenticationScheme is useful when there are multiple instances of cookie authentication and you want to authorize with a specific scheme

You can see that we have specified the cookie expiration time to be 1 minute and we also have enabled sliding expiration for the cookie.

Startup.Configure method

In Startup.Configure method, call UseAuthentication and UseAuthorization methods before calling Use Endpoints. These methods set HttpContext.User property and run the authentication middleware for requests.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
view raw startup.cs hosted with ❤ by GitHub

AuthController Code

Let’s add a new controller with name AuthController to the project inside the Controllers directory. We will add three methods to this controller – Register, Login and Logout.

The Register method will add a user to the Identity tables. I created this method because it will be handy to test the login / logout functionality. The Login method will validate the credentials and Logout methods will sign out the user.

The CreateAsync method creates the Identity User in the database. It accepts a clear text password which is hashed and then stored in the AspNetUsers table.

The FindByNameAsync method finds out the identity by username. So it can be used in Login method to check if user with provided credentials exists.

Below is the complete code of the AuthController.cs file.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using CookieAuthSampleAPI.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace CookieAuthSampleAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly SignInManager<IdentityUser> signInManager;
private readonly UserManager<IdentityUser> userManager;
public AuthController(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager)
{
this.signInManager = signInManager;
this.userManager = userManager;
}
[HttpPost]
[Route("Register")]
public async Task<IActionResult> Register([FromBody]UserDetails userDetails)
{
if (!ModelState.IsValid || userDetails == null)
{
return new BadRequestObjectResult(new { Message = "User Registration Failed" });
}
var identityUser = new IdentityUser() { UserName = userDetails.UserName, Email = userDetails.Email };
var result = await userManager.CreateAsync(identityUser, userDetails.Password);
if (!result.Succeeded)
{
var dictionary = new ModelStateDictionary();
foreach (IdentityError error in result.Errors)
{
dictionary.AddModelError(error.Code, error.Description);
}
return new BadRequestObjectResult(new { Message = "User Registration Failed", Errors = dictionary });
}
return Ok(new { Message = "User Reigstration Successful" });
}
[HttpPost]
[Route("Login")]
public async Task<IActionResult> Login([FromBody]LoginCredentials credentials)
{
if (!ModelState.IsValid || credentials == null)
{
return new BadRequestObjectResult(new { Message = "Login failed" });
}
var identityUser = await userManager.FindByNameAsync(credentials.Username);
if (identityUser == null)
{
return new BadRequestObjectResult(new { Message = "Login failed" });
}
var result = userManager.PasswordHasher.VerifyHashedPassword(identityUser, identityUser.PasswordHash, credentials.Password);
if (result == PasswordVerificationResult.Failed)
{
return new BadRequestObjectResult(new { Message = "Login failed" });
}
var claims = new List<Claim>
{
new Claim(ClaimTypes.Email, identityUser.Email),
new Claim(ClaimTypes.Name, identityUser.UserName)
};
var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity));
return Ok(new { Message = "You are logged in" });
}
[HttpPost]
[Route("Logout")]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Ok(new { Message = "You are logged out" });
}
}
}
view raw AuthController.cs hosted with ❤ by GitHub

Authorize

Even if login and logout is working properly from your AuthController, it is not helpful yet. This is because the WeatherForecastController is actually allowing anonymous calls.

So, for completing the demo, let’s apply the [Authorize] attribute on the WeatherForecastController as shown below. You can specify the name of Authentication Scheme which will allow Authorize attribute to use appropriate scheme for verifying authorization.

[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]

Now if you try to call the GET action on WeatherForecastController, it would return 404.

Wait…. what ?

Why 404 instead of 401 ?

Well, actually, I was expecting it to return 401 unauthorized. But there seems to be an issue in the .NET Core 3 cookie authentication middleware, it tries to redirect the GET call to Account/Login action, which does not exist in our solution. This is the reason why we get 404 Not Found.

I also tried to change the LoginPath on cookie policy, to /Auth/Login which exist in our solution. But it was still trying to redirect the user to Account controller which looks odd.

If you are more curios, refer this issue and this issue on GitHub.

Test in Postman

If your solution is ready, you can test the APIs using Postman. Create the User Registration, Login, Get weather forecast and Logout requests and execute them in the same order for testing the positive flow.

If you are too lazy (like me) to create the Postman requests, you can also use the JSON file from the GitHub solution and import it in the Postman. It will have sample inputs for these for requests..

When you perform login, you do not need to add any additional inputs to Weather Forecast GET request, that is because, Postman automatically uses the cookie which was issued after successful login.

You can download the complete source code of cookie authentication sample from my GitHub repository.

I hope you enjoyed this article. Let me know your thoughts.

Leave a Reply

This Post Has 4 Comments

  1. Pratik

    Thank you for the amazing post.
    404 issue got resolved by adding below Key Value pair.
    X-Requested-With : XMLHttpRequest

  2. Edgar Torres

    Hi,

    Thank you for your amazing post, I got fix the issue 404 code, adding CookieAuthenticationDefaults.AuthenticationScheme as I copied below. I hope help to others.

    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
    {
    options.Events.OnRedirectToLogin = context =>
    {
    context.Response.Headers[“Location”] = context.RedirectUri;
    context.Response.StatusCode = 401;
    return Task.CompletedTask;
    };
    options.SlidingExpiration = true;
    options.ExpireTimeSpan = new TimeSpan(0, 1, 0);

    });

    1. Manoj Choudhari

      That is also nice approach. As an alternative and if it is possible for you to change API client, you can also try sending this header ‘X-Requested-With : XMLHttpRequest’ with request as mentioned on the GitHub links. If this header approach works for you, then you do not need the code to override OnRedirectToLogin.