Web API + Blazor (Server-side) w/ JWT and Cookie auth

I have two .NET 5 projects:

  • A WebAPI that provides data to a Xamarin mobile app (with Auth0 Xamarin libs)
  • A Blazor server-side project for admin management of the data that feeds the WebAPI

I successfully wired up each these projects with Auth0 and three identity providers (Facebook, Google, Twitter). I can successfully login/logout out of my mobile app with Auth0, and I can successfully login/logout of the Blazor app. I have also successfully wired up role-based-auth for each; I have my Blazor pages decorated with the role-based [Authorize(Roles = "MyCustomRole")] annotation, and the pages properly allow only users with the correct role to access them. I have not yet added role-based auth annotations to my WebAPI controllers and methods, but I can see the roles represented in the decoded JWT, so I know that roles are present in the ClaimsPrincipal (user). The WebAPI app uses JWT. The Blazor app uses cookie-based session auth. I was able to get both of these working by following Auth0 how-to articles.

I decided to combine these two separate projects into a single .NET project, primarily so that I can host a single web application instead of two. I accomplished this combination of projects successfully; the Blazor app and the WebAPI now run in the same host instance.

However…I can’t seem to get the app to use both JWT and cookies. I’m not sure, but it seems like it may be because I have to specify DefaultAuthenticateScheme, DefaultSignInScheme, and DefaultChallengeScheme as either cookie-based or JWT-based; and I can’t specify both:

services
.AddAuthentication(
    options => {
        // I believe this may be where the problem is: I can't specify both cookie auth AND JWT auth; it has to be one or the other.
        options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    })

If I set these options using CookieAuthenticationDefaults.AuthenticationScheme, then role-based auth works properly in the Blazor app, but the ClaimsPrincipal in my WebAPI app contains an empty Claims collection.

If I set these options using JwtBearerDefaults.AuthenticationScheme, then role-based auth stops working in the Blazor app, but in the WebAPI project I see the Claims collection property contains items, one of which has the expected role values.

Am I correct in my assessment that I can’t specify both cookie-based auth and JWT-based auth in the same hosted application? Will I be forced to keep the projects separate and run them in separate host instances so that I can specify different auth types for each?

Here’s a gist of my Startup.cs:

I may have found the answer to my own question, but I’ll verify it later tonight.

I believe the answer may be that I need to explicitly specify the AuthenticationScheme in every place that I use the [Authorize] attribute.

So, in my Blazor pages, I’ll do something like this:

@using Microsoft.AspNetCore.Authentication.Cookies
@attribute [Authorize(Roles = "MyCustomRole", AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]

And in my WebAPI controllers, something like this:

[ApiController]
[Route("api/[controller]")]
[Authorize(Roles = "MyCustomRole", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class EventController : BaseController
{
    ...
}

Not the most glamorous solution, but as long it works, I’ll be happy.

I’ll post back here later with findings.

2 Likes

Unfortunately doesn’t have much of experience with this part of the stack but thanks a lot for exploring all that and sharing it with the rest of community!

It turns out that what I mentioned in my potential solution DID NOT work. However, I realized that I was accidentally using two different domain values in my various configs. I have a custom domain in Auth0 that I setup many many months ago, and then forgot about it when I picked up this project again recently. So I was using a mix of [mydomain].auth0.com and login.[mydomain].com across various configs, which will of course cause issues because everything needs to be referencing the same authority. Plus, I was forgetting to logout of the mobile app between tests (which I tuck away into device secure storage), so I was trying to pass an old identity with a different claim authority to my Web API instance that had since seen its config updated to the new custom domain authority value. That’s a recipe for a whole lotta nope. :man_facepalming:

So, I had several issues conflating my understanding of the problems I was seeing yesterday.

I still don’t have Auth0 login working for my Blazor project (the callback after login fails), so I still need to track that down. But at least the ClaimsPrincipal (the .NET representation of the JWT token) is coming through successfully on my Web API controllers. So, I’m halfway there.

FIGURED IT OUT!

For Blazor to work properly with Auth0, these options must be set:

services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})

That’s nothing new; it’s outlined in the Auth0 article by Andrea Chiarelli here:

But then on my API controllers, I need to specifically set the authentication scheme with the [Authorize] attribute. If I don’t do this, the ASP-NET doesn’t attempt to decode the JWT from the header into a ClaimsPrincipal (which is required for auth and mapping to permissions/roles).

[HttpGet]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<ActionResult<MyCustomResponseModel>> Get([FromQuery] MyCustomRequestModel request)
{
    ...
}

After I login to my Xamarin app using the Auth0 component, I store the JWT in the device’s secure storage. On each request to my Web API (after checking the refresh token for expiration validity), I pass the token in an authorization Bearer header. (I use the Refit library for building the client that talks to my WebAPI). My Web API has a mix of public and secure endpoints. On the secure endpoints, I put the [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] annotation to force .NET to decode the JWT into a ClaimsPrincipal that’s automatically exposed on from ASP-NET’s Controller Base as the User property.

So, that’s how it all worked out! Now I have a single .NET 5 ASP-NET project that contains a Blazor app, Web API controllers, supports both JWT and cookie authentication, and all runs in a single hosting instance. :+1: :+1: :+1:

Here’s my Startup.cs. There’s a few extra things in there that aren’t specific to Auth0 setup, but a large chunk of it is.

Here’s most of the Auth0 articles that helped me figure all of this out. These articles don’t cover every single bit of knowledge that helped me figure this out, but these are definitely important.

https://auth0.com/docs/quickstart/webapp/aspnet-core-3/01-login?_ga=2.92752093.1348887534.1606429229-162955205.1606429227#install-and-configure-openid-connect-middleware

1 Like

Thanks for sharing it with the rest of community!

I created a full demo project for this, using the default .NET 5 Web API and Blazor templates:

All someone has to do is setup their Auth0 account, and then plug in their Auth0 values for domain, audience, client ID, and client secret.

1 Like

That sounds even more perfect! Thank you a lot for doing that!