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:
- The front-end runs on http://localhost:4000, unsecure
- The back-end runs on https://localhost:7033
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?