.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).