Combining cookie and JWT bearer authentication in ASP.NET Core

This post discusses how to combine cookie authentication with JWT bearer authentication in an ASP.NET Core application without manipulating the token in any way – like it is suggested in several articles on the web.

Disclaimer: The approach described in this article works with ASP.NET Core 3.0 and later versions.

Recently, while developing a single page application with Angular and an ASP.NET Core 3 backend, we got the requirement to integrate pre-generated HTML sites. These sites were self-contained, and each consisted of a bunch of JavaScript, JSON, CSS and image files. In order to make them available, we host these view packages in a flat file storage and let the user access them via an API controller that is part of our REST API. You might be asking why we are doing this using such an odd approach. The reason is, access to these sites has to be restricted to only specific users which are already present in our SPA.

Such an HTML site consists of many assets – as mentioned above – that all require access restrictions. Since we want to avoid having to manipulate any of the contents (e.g. by setting a Bearer header for XHR requests), we needed a way to provide the user information (i.e. their claims) to our API controller. Cookies were the obvious approach, since we also host everything (the SPA and the REST API) under the same domain. The API controller uses the [Authorize] attribute to require basic authorization.

Setting up the authentication pipeline

As the first step, we add the cookie authentication middleware to the ASP.NET pipeline. As you can see in the code below, this is done in addition to the JWT bearer middleware that is already in use by the SPA.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// JwtBearerDefaults.AuthenticationScheme == "Bearer"
// CookieAuthenticationDefaults.AuthenticationScheme == "Cookie"

services
  .AddAuthentication(o =>
  {
    o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
  })
  .AddJwtBearer()
  .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, o =>
  {
    o.ExpireTimeSpan = TimeSpan.FromMinutes(30); // optional
  });

Defining the multi-scheme policy

As a second step we have to create a new authorization policy multiSchemePolicy that causes ASP.NET to pick up information from both authentication mechanisms. Setting this as the default policy, well, makes it the default for all requests to controller methods that require authorization. The following code shows how this policy is built using the AuthorizationPolicyBuilder class.

1
2
3
4
5
6
7
var multiSchemePolicy = new AuthorizationPolicyBuilder(
    CookieAuthenticationDefaults.AuthenticationScheme,
    JwtBearerDefaults.AuthenticationScheme)
  .RequireAuthenticatedUser()
  .Build();

services.AddAuthorization(o => o.DefaultPolicy = multiSchemePolicy);

Signing in using the cookies scheme

As the last step, we want to set the cookie without explicitly requiring the user to provide their credentials again. Since the user is already signed into the single page application, we provide a REST endpoint that is called by the SPA before accessing the pre-generated HTML sites. The most simple approach could look like the following code.

1
2
3
4
5
6
7
8
9
10
[Authorize]
[HttpPost("setcookie")]
public async Task<ActionResult> SignInWithCookie()
{
  await HttpContext.SignInAsync(
    CookieAuthenticationDefaults.AuthenticationScheme, 
    HttpContext.User);

  return NoContent();
}

By calling HttpContext.SignInAsync(...) a cookie with encrypted content is set, which name defaults to .AspNetCore.Cookies. In the browser this might appear in multiple chunks.

Cookie set by ASP.NET Core consisting of two chunks Fig. 1: Cookie set by ASP.NET Core consisting of two chunks

Further considerations

Bear in mind that when the cookie is set for the same domain as the SPA is using, the cookie is now also transmitted with every subsequent XHR request by the SPA. But if you configured the policy as explained above, ASP.NET Core is smart enough to merge both schemes into a single user with a single identity that can be access via HttpContext.User. On the other hand you cannot easily infer which scheme was used for authorization when accessing the user in the HTTP context.

Additionally, we now have to make sure the cookie is deleted once the user signs out of the single page application. Otherwise you will encounter problems when a different user uses the application but the cookie is still sent from the previous user.

In case you need more a more sophisticated authorization logic, this also works with the described multi-scheme policy. You simply have to register additional policies when adding the authorization middleware and extend the authorization attribute on the controller with the policy name, e.g. [Authorize("MySuperAdminPolicy")]:

1
2
3
4
5
6
7
8
services
  .AddAuthorization(o =>
  {
    o.DefaultPolicy = multiSchemePolicy;
    o.AddPolicy(
      "MySuperAdminPolicy", 
      b => b.Requirements.Add(new MySuperAdminRequirement()));
  });

I hope you enjoyed this blog post and found it useful. Maybe it helped you implementing a similar odd requirement. I’m open to your thoughts on this subject, or how to improve this post.

Thank you!

Title photo by kalei peek