.NET Core Authentication with React front-end problems

Hello, I’m trying to set up Auth0 in my .NET core web API backend, with a React front-end that simply passes through the cookie created by the API in order to validate that the requests are coming from an authenticated user. I’m struggling a lot with the pattern that I’m trying to use, which is as follows:

  • User visits the front-end (any page) which makes a request to an endpoint in .NET core with the [Authorize] attribute to fetch user information
  • Backend verifies their authentication status, and if they are not authenticated, returns a 401
  • Front-end intercepts 401 and redirects user to web-API backend endpoint /account/login (set up to challenge authentication)
  • Auth challenge redirects to auth0, normal process of authenticating user occurs
  • Backend receives token information, sets cookie, and returns to front-end
  • Front-end makes requests normally now, forwarding the auth cookie normally to any requests to the backend

Frustratingly, this workflow fails almost immediately due to the request pipeline always sending along a 302 response code to the frontend whenever an endpoint with the [Authorize] attribute runs. Since requests from the front-end are XHR, I don’t want to redirect to authentication in that situation - the browser can’t follow redirects like that, so I need to manually handle 401s to automatically send the user to re-authenticate. Additionally, for testing purposes, i’m running my front-end without an SSL certificate, and so I have a cookie policy set up to not use secure cookies. This also doesn’t work at all, which is arguably more important since I could at least secure my application this way, and deal with the request pipeline in other ways.

Other things that may be important to note, based on the above:

I’ve tried many different things and none of them seem to work. Any changes I make to the authorization policy or pipeline are always overridden and I never get a 401 back. Additionally, looking at the Set-Cookie headers on callback to the .NET application, I can see that it clearly is adding a secure cookie, despite trying everything I can in the configuration to not do so for my local environment. Is there something I’m missing here? My current code is below:

Program.cs

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddCors(opt =>
{
    opt.AddPolicy("DefaultPolicy", builder =>
    {
        builder.WithOrigins("https://localhost:4000", "http://localhost:4000")
            .AllowAnyHeader()
            .AllowAnyMethod()
            .SetIsOriginAllowed((x) => true)
            .AllowCredentials();
    });
});

builder.Services.Configure<CookiePolicyOptions>(options =>
{
    options.MinimumSameSitePolicy = SameSiteMode.None;
    options.HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always;

    if (builder.Environment.IsDevelopment())
    {
        // Development environment - set cookies without the Secure attribute
        options.Secure = CookieSecurePolicy.None;
    }
    else
    {
        // Production environment - set cookies with the Secure attribute
        options.Secure = CookieSecurePolicy.Always;
    }
})
    .ConfigureApplicationCookie(options =>
{
    options.Cookie.SameSite = SameSiteMode.None;
    options.Cookie.HttpOnly = true;

    if (builder.Environment.IsDevelopment())
    {
        // Development environment - set cookies without the Secure attribute
        options.Cookie.SecurePolicy = CookieSecurePolicy.None;
    }
    else
    {
        // Production environment - set cookies with the Secure attribute
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    }
});

builder.Services.AddAuth0WebAppAuthentication(options =>
{
    options.Domain = builder.Configuration["Auth0:Domain"];
    options.ClientId = builder.Configuration["Auth0:ClientId"];
    options.CallbackPath = "/callback";
});

builder.Services.AddAuthorization(
    options =>
    {
        options.AddPolicy("RequiredUserPolicy", opt =>
        {
            opt.AddAuthenticationSchemes(Auth0Constants.AuthenticationScheme).RequireAuthenticatedUser();
        });
    }
);

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseCors("DefaultPolicy");

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

AccountController.cs
(initial front-end load attempts to hit /account/userinfo)

 [Route("account")]
    public class AccountController : Controller
    {
        private readonly string _redirectUrl;
        ...

        [HttpGet("login")]
        public async Task Login([FromQuery]string returnUrl = "/")
        {
            var authenticationProperties = new LoginAuthenticationPropertiesBuilder()
                // Indicate here where Auth0 should redirect the user after a login.
                // Note that the resulting absolute Uri must be added to the
                // **Allowed Callback URLs** settings for the app.
                .WithRedirectUri(_redirectUrl)
                .Build();

            await HttpContext.ChallengeAsync(Auth0Constants.AuthenticationScheme, authenticationProperties);

            //return Task.FromResult(Redirect(_callbackUrl));
        }

        [Authorize(Policy = "RequiredUserPolicy", AuthenticationSchemes = "Auth0")]
        [HttpGet("userinfo")]
        public async Task<IActionResult> GetUserInfo()
        {
            var claimsEmail = User.Claims.FirstOrDefault(x => x.Type == "email");

            if(claimsEmail == null)
            {
                throw new ArgumentNullException("Couldn't get email from claims.");
            }

            var user = await _context.Users
                .Where(x => x.Email == claimsEmail.Value)
                .ProjectToType<AuthenticatedUser>()
                .FirstOrDefaultAsync();

            if (claimsEmail == null)
            {
                throw new ArgumentNullException($"Couldn't find user with matching email '{claimsEmail}'");
            }

            return Ok(user);
        }
    }

I feel a bit crazy that not even one of the pieces of this are working based on how I’d like to do it. I am open to changing this architecture, but I assumed that this would be a pretty straightforward way of ensuring users are automatically authenticated and any requests that go through to the back-end automatically challenge the user again. Is there something inherently wrong with my setup?

Follow-up - I wasn’t able to get the cookie to actually get sent from the front-end while making the request from a non-secure context, but setting up a proxy for my nextjs project resolved that issue. However, I’m still not able to overwrite the behavior of the Authorize attribute for some reason. I would still love to hear from someone on whether or not it’s possible to return 401s all of the time instead of the current 302 redirect behavior (from authenticated requests).

Hello. Just wondering if you were able to resolve your issue ? I am encountering similar behaviors and results and hoping there is a consistent approach for using this pattern… Note that my front end app happens to run from a different

Unfortunately, I didn’t have any luck getting 401s to return natively. It seems that there’s some piece of the pipeline that I can’t access due to the auth0 middleware doing some magic behind the scenes. What worked for me was using fetch or another httprequest library that does (such as wretch) on the front-end to make the request to login, which fails in a more predictable manner when the .NET API sends a 301 through. So for example, I have wretch making the call and if the redirect occurs, then redirect the user manually. Something like this:

wretch(`${process.env.NEXT_PUBLIC_API_V2_URL}`)
  .options({ credentials: "include", mode: "cors", redirect: "manual" })
  .catcherFallback((err) => {
    const isUserInfo = err.url.endsWith("/account/userinfo");

    if (isUserInfo && (!err.status || err.status === 0)) {
      redirectToLogin();
    } else if (isUserInfo && err.status === 401) {
      logout();
    }
  });

here, the userinfo endpoint is the first network request that happens when the users hits my front-end. hope this is helpful in some way.