Set-cookie not working for cross-site request / response in Dot net Core 3.1 & React setup same-site cookies and/or CORS issue

Viewed 4566

Alright, I am stumped! I have been trying to solve this for hours now with no luck. I am following this guide to use JWT for auth in a Dot Net Core 3.1 / React.js (typescript) project I am working on to learn the whole setup. I am working using cross site requests. My React server is communicating on https://localhost:3000 (dev using Visual Studio Code), and my API / back end, API server is running on https://localhost:44309 running in Visual Studio.

I am trying to send a refresh token back to the client and the guide states this needs to be in a HTTP Only cookie to mitigate XSS. No matter what I try, I cannot get the browser to execute the ‘set-cookie’ at the client side so I can see it in Google Chrome's Dev Tools > Application > Cookies. This is for any cookie that I set in the response at all. If I use Google Chrome’s Developer Tools panel, in the network response I can see the ‘set-cookie’ headers are there, but they never show in ‘Application > Cookies > LocalHost’. The response I am sending sends the payloads and that can be used / read with no issue. It just will not set cookies!

The setup works fine when I use the same server for client and server application parts (just run it in IIS in the standard Visual Studio setup); any / all cookies set with no problems, so I am guessing I am working with a cross-site issue. I just do not know how to fix it.

My code:

//setting up cors
services.AddCors(options =>
{
  options.AddPolicy("CORSAllowLocalHost3000",
    builder =>
    builder.WithOrigins("https://localhost:3000")
      .AllowAnyHeader()
      .AllowAnyMethod()
      .AllowCredentials()
   );
});

//using cors
app.UseCors("CORSAllowLocalHost3000");


//setting up auth
services
.AddDefaultIdentity<ApplicationUser>
  (
    options =>
    {
      options.SignIn.RequireConfirmedAccount = false;
      options.Password.RequiredLength = 6;
    }
  )
.AddEntityFrameworkStores<IdentityApplicationContext>();


services
.AddAuthentication(opts =>
{
  opts.DefaultAuthenticateScheme = "JwtBearer";
  opts.DefaultScheme = "JwtBearer";
  opts.DefaultChallengeScheme = "JwtBearer";

})

.AddJwtBearer("JwtBearer", opts =>
{
  opts.SaveToken = true;
  opts.TokenValidationParameters = tokenValParams;
});

//tokenValParams to validate the JWT
var tokenValParams = new TokenValidationParameters
{
  ValidateIssuerSigningKey = true,
  IssuerSigningKey = new SymmetricSecurityKey(key:  
Encoding.ASCII.GetBytes(configuration.GetSection("Authentication").GetSection("JwtBearer").GetSection("SecurityKey").Value)),
  ValidateIssuer = false,
  ValidateAudience = false,
  RequireExpirationTime = false,
  ValidateLifetime = true,
  ClockSkew = TimeSpan.Zero
};


//for API only dev env, start as API only service - no browser - client app runs 
app.UseStaticFiles();
if (!env.IsEnvironment("APIOnlyDevelopment")) 
  app.UseSpaStaticFiles();

app.UseSpa(spa =>
{
  spa.Options.SourcePath = "ClientApp";
  if (env.IsDevelopment())
    {
      spa.UseReactDevelopmentServer(npmScript: "start");
       //spa.UseProxyToSpaDevelopmentServer("https://localhost:3000");  
     });


//Test Cookie generated in client before async Ok() result is returned in controller
HttpContext.Response.Cookies.Append("JwtRefreshTokenn", resultOfSignin.RefreshToken, new CookieOptions() { Secure = true, HttpOnly = true, SameSite = SameSiteMode.None});

Response.Cookies.Append("JwtRefreshTokenn2", resultOfSignin.RefreshToken, new CookieOptions() { HttpOnly = true, Secure = true, SameSite = SameSiteMode.None});

Response.Cookies.Append("JwtRefreshTokenn3", resultOfSignin.RefreshToken, new CookieOptions() { });

Further information:

  • I have changed the ‘App URL’ in Visual Studio to that of the ‘Enable SSL’ URL as I was getting a CORS issue with a redirect that was occurring.
  • I am running the server using the inbuilt ‘HTTPS’ setup, and the client app using npm’s https setup
    (including sorting the cert error out as with this post).
  • I have tried all combinations of cookie options, including adding domains / paths (and all variations of same-site attribute)
  • I have tried different things in the CORS policy (e.g. omitting .AllowCredentials)
  • I have tried using http rather than https
  • Firefox is still having CORS issues with the requests that Google Chrome is not
  • The problem is mirrored in MS Edge
  • All running in Windows 10
  • I am relatively new to this, so please let me know if I have missed anything out.

Any helps is greatly appreciated. Many thanks, Paul

1 Answers

After a good few hours invested in this, it was a simple fix that was required on the client. The setup above is / was good and working, with a particular mention to this:

services.AddCors(options =>
{
  options.AddPolicy("CORSAllowLocalHost3000",
    builder =>
    builder.WithOrigins("https://localhost:3000")
      .AllowAnyHeader()
      .AllowAnyMethod()
      .AllowCredentials() // <<< this is required for cookies to be set on the client - sets the 'Access-Control-Allow-Credentials' to true
   );
});

As the client is using axios to make API calls from within React, a global default was needed to be set to match this / work with this header. So at least in one place in the program where axios is imported, the default is set as so:

import axios from 'axios';
axios.defaults.withCredentials = true;

I had a custom axios creator file setup so that I could use e.g. interceptors etc, which is where I put this code. Once these two things were added & aligned, the cookies were being set.

Hope it helps.

Related