Search code examples
javascriptcookiesfetch-apicredentials

fetch sets same-origin cookie locally but same code doesn't set cookie when deployed


I'm tearing my hair out. Please help! I have a user interface SPA app which is the frond end of an identity application. The application has a login API which sets an auth cookie. The SPA posts login credentials to this login API endpoint using a fetch function that has credentials: "same-origin" because it expects a Set-Cookie header on the response containing a session cookie. It then redirects the browser to a callback endpoint on the same domain as the API which requires the session cookie in the request.

When I run this locally it works as expected, and here's the trace:

  1. Login API request:
POST https://identity.mywebsite.local:44119/auth/email HTTP/1.1
Host: identity.mywebsite.local:44119
Connection: keep-alive
Content-Length: 39
sec-ch-ua: "Microsoft Edge";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
sec-ch-ua-platform: "Windows"
DNT: 1
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0
Content-Type: application/x-www-form-urlencoded
Accept: */*
Origin: http://my.mywebsite.local:3100
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://my.mywebsite.local:3100/
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-GB,en;q=0.9

[email protected]&password=mypassword
  1. Login API response (note the mywebsite-identity cookie being set):
HTTP/1.1 200 OK
Content-Length: 0
Date: Fri, 26 Apr 2024 11:00:50 GMT
Server: Kestrel
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://my.mywebsite.local:3100
Cache-Control: no-cache,no-store
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Pragma: no-cache
Set-Cookie: mywebsite-session=1F13A118FA46BD80ED830D1AFEBDAA0D; path=/; secure; samesite=none
Set-Cookie: mywebsite-identity=[value]; expires=Fri, 10 May 2024 11:00:51 GMT; path=/; secure; samesite=none; httponly
R-Source: identity
  1. Callback request (note the mywebsite-identity cookie being sent):
GET https://identity.mywebsite.local:44119/connect/authorize/callback?client_id=www&... HTTP/1.1
Host: identity.mywebsite.local:44119
Connection: keep-alive
sec-ch-ua: "Microsoft Edge";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
DNT: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://my.mywebsite.local:3100/
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-GB,en;q=0.9
Cookie: mywebsite-session=1F13A118FA46BD80ED830D1AFEBDAA0D; mywebsite-identity=[value]

But when I deploy this exact code to a remote environment the browser is no longer setting the cookie received in the API response. Here's the trace:

  1. Login API request:
POST https://identity-build.mywebsite.org.uk/auth/email HTTP/1.1
Host: identity-build.mywebsite.org.uk
Connection: keep-alive
Content-Length: 50
sec-ch-ua: "Microsoft Edge";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
sec-ch-ua-platform: "Android"
DNT: 1
sec-ch-ua-mobile: ?1
User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile Safari/537.36 Edg/123.0.0.0
Content-Type: application/x-www-form-urlencoded
Accept: */*
Origin: https://my-build.mywebsite.org.uk
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://my-build.mywebsite.org.uk/
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-GB,en;q=0.9

[email protected]&password=mypassword1
  1. Login API response (note the mywebsite-identity cookie being set as expected):
HTTP/1.1 200 OK
Date: Fri, 26 Apr 2024 11:03:00 GMT
Content-Length: 0
Connection: keep-alive
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://my-build.mywebsite.org.uk
Cache-Control: no-cache,no-store
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Pragma: no-cache
Set-Cookie: mywebsite-session=1C5F6777E03D96BD215029199901B927; path=/; samesite=none
Set-Cookie: mywebsite-identity=[value]; expires=Fri, 10 May 2024 11:03:00 GMT; path=/; samesite=none; httponly
Request-Context: appId=cid-v1:fff610af-e464-4fb4-8ece-c73f6587e07b
R-Source: identity
CF-Cache-Status: DYNAMIC
Report-To: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=WHaIgYERtzjcCm5robKrgl0nLh0XZSHWmaPoU48%2B5MTDDWgVXSs9MO%2Fqe2q4lgutPrR6HdSEAfFbNlh42Xmzw0AEU2TqIlEbynCNJVt4Qkd7VZ3TxVLfM2kjxut6br84jgUmYR2C9HjFj0RaG5"}],"group":"cf-nel","max_age":604800}
NEL: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
Server: cloudflare
CF-RAY: 87a6156f2b3071e1-LHR
alt-svc: h3=":443"; ma=86400
  1. Callback request (notice no cookies are sent because the browser didn't set them from the previous response):
GET https://identity-build.mywebsite.org.uk/connect/authorize/callback?client_id=www&... HTTP/1.1
Host: identity-build.mywebsite.org.uk
Connection: keep-alive
Upgrade-Insecure-Requests: 1
DNT: 1
User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile Safari/537.36 Edg/123.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
sec-ch-ua: "Microsoft Edge";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
sec-ch-ua-mobile: ?1
sec-ch-ua-platform: "Android"
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-GB,en;q=0.9

I cannot see what's different enough to have this impact.

In case it's useful, the SPA is a nextjs app (where the API calls are client side) and the fetch code is as simple as this:

fetch(url, {
  method: "POST",
  credentials: "same-origin",
  headers:{
    'Content-Type': 'application/x-www-form-urlencoded'
  },    
  body: formData
})

I should mention that although the API is running on identity-build.mywebsite.org.uk (which is also the domain I expect the cookie to be set on), the SPA which calls it is running on my-build.mywebsite.org.uk. My assumption was the origin we're concerned about here (in terms of browser cookie security) is the API's domain.


Solution

  • In your dev configuration a secure flag is sent in the Set-Cookie response headers, whereas in the build configuration, a secure flag is not included. In both the dev and build configs, the Set-Cookie headers include a samesite=none flag. If samesite=none is set, the secure flag must be included otherwise the cookie is blocked. See Set-Cookie on MDN Docs.

    To resolve this, ensure you are using a valid HTTPS configuration and edit the response headers to ensure Set-Cookie includes a secure flag.

    Alternatively, since the sites are subdomains of the same domain, the request is not considered cross-origin and thus the samesite=none flag does not need to be included for the cookie to be sent in the request. Removing the samesite=none flag could resolve the problem and reduce the risk of CSRF.

    It is strange that the API sends a valid Set-Cookie header in dev but not in build. If you control the API you can follow the steps above to resolve the problem. If you do not control the API you should first check your HTTPS configuration is valid and then contact the developer.