Search code examples
javascriptcookiescross-domainhttponlycookie-httponly

Missing HTTPOnly Cookies at HTTP Request from child iFrame or pop-up window


Browser (Chrome) doesn't set HttpOnly cookies from child iframe or pop-up window

I have a parent webpage with a child iframe:

Parent at https://sub1.some-domain.com
Child at <iframe src="https://sub2.some-domain.com"> (inside of parent)

From Parent I do POST request to the API https://api.some-domain.com.
Below is the Response headers:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://sub1.some-domain.com
Set-Cookie:
 payload-name=payload-value;
 max-age=30;
 domain=some-domain.com;
 path=/;
 secure;
 samesite=none;
 httponly
    

Afterwards,
from the Iframe I do GET request to "https://api.some-domain.com".
Expectation: Browser to include Httponly cookies payload-name=payload-value; into the request.

Result: Httplonly cookies not included for unknown reason.
BTW, I included "withCredentials" property into JS Http request, so this couldn't be a problem.

Update

GET Request:

fetch("https://api.some-domain.com/...", {
  "headers": {
    "accept": "application/json",
    "accept-language": "en-US,en;q=0.9,uk;q=0.8",
    "cache-control": "no-cache",
    "content-type": "application/json",
    "pragma": "no-cache",
    "sec-ch-ua": "\"Not/A)Brand\";v=\"99\", \"Google Chrome\";v=\"115\", \"Chromium\";v=\"115\"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "\"Windows\"",
    "sec-fetch-dest": "empty",
    "sec-fetch-mode": "cors",
    "sec-fetch-site": "same-site"
  },
  "referrer": "https://sub2.some-domain.com/",
  "referrerPolicy": "strict-origin-when-cross-origin",
  "body": null,
  "method": "GET",
  "mode": "cors",
  "credentials": "include"
});

GET Response headers:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://sub2.some-domain.com
Cache-Control: no-store,no-cache
Content-Encoding: gzip
Content-Type: application/json; charset=utf-8
Pragma: no-cache

Update2
Locally, everything works fine, httponly cookies included.
I believe this reason is that https://api.some-domain.com, https://sub1.some-domain.com, https://sub2.some-domain.com is https://localhost.
Update3
The same is issue is when instead of iframe is pop-up window.


Solution

  • The latest cookie behaviours and restrictions are explained in recent updates to the RFC6265 document. There have been updates in recent years to both browsers and servers, to prevent unwelcome cross site user tracking.

    SAME SITE

    All of the subdomains you are using share the same parent domain of some-domain.com. Assuming that this is not a shared domain from a hosting provider, and that you own the whole domain, this means all requests are in the same site and should not be impacted by cookie restrictions. Section 5.2 describes the algorithm.

    If the browser domain and the API domain are not in the same site, then cookies will be treated as third party and dropped aggressively, especially on iframe and Ajax requests, where the user is not informed of the origin making the API request.

    CROSS ORIGIN

    Requests from the first two of these domains to the API will be cross origin and same site, since in both cases the hostname of the API and browser document is not the same. Therefore cross origin requests will trigger CORS pre-flight OPTIONS requests, which you seem to be dealing with correctly.

    sub1.some-domain.com
    sub2.some-domain.com
    api.some-domain.com
    

    Since you are calling the API from two web origins, make sure the CORS response headers returned for pre-flight OPTIONS requests vary by origin. I suspect that is already the case though:

    Vary: Origin, Access-Control-Request-Headers
    

    COOKIE DETAILS

    If you are using authentication cookies to secure requests from the browser to APIs, then I would recommend against using SameSite=None. This is intended only for special cases, notably third party cookies issued by authorization servers and identity providers. This prevents cookies used for logins on a top level window from being dropped. I would recommend updating your scenario to use cookie details like this:

    Set-Cookie:
     payload-name=payload-value;
     max-age=30;
     domain=api.some-domain.com;
     path=/;
     secure;
     samesite=strict;
     httponly
    

    The strict setting provides the strongest protection against cross site request forgery, unlike the none setting. This also ensures that cookies are only sent to the API side of the architecture. If you are using API driven cookies in the browser, you should not need to use the cookie for web requests, if you are just getting static content. In some deployment scenarios, such as when getting web content from a content delivery network, it is more secure to avoid sending an API credential there.

    FIRST PARTY IFRAMES

    Assuming a same site setup, the iframe document for sub2.some-domain.com and the main window's top level origin of sub1.some-domain.com will be considered to have the same site for cookies. The iframe will send an origin of https://sub2.some-domain.com to the API and shared cookies work OK.

    Use of first party iframes still has some limitations though. If you ever wanted to trigger an OAuth based login from an iframe, eg to sign in via Google, that would not work since the login cookies would be considered third party and dropped.

    THIRD PARTY IFRAMES

    If the same site requirement is not met, then cookies will be dropped by browsers and you won't be able to influence that. Third party iframes that use cookies therefore need to be migrated to first party iframes. This typically involves hosting the iframe in your own web domain, then routing API requests via an API domain in the same site as the web origin.

    FETCH REQUESTS

    Make sure that both POST request with "Set-Credentials: .." header and next GET request has "withCredentials" option. If POST request would not have it, the cookies wouldn't be set, even if you see them in DEV Tools.

    Simplify your FETCH request, since request headers add complexity. Any non safe headers you send must have been first requested in a pre-flight OPTIONS request, resulting in response headers like this:

    Access-Control-Allow-Headers: content-type
    

    Simplify it something like this. Mostly the browser code only needs to opt into sending cross origin cookies. You should not need to try to manipulate the request in other ways.

    fetch("https://api.some-domain.com/...", {
      "headers": {
        "accept": "application/json",
        "content-type": "application/json"
      },
      "method": "GET",
      "mode": "cors",
      "credentials": "include"
    });
    

    My solutions have mostly used axios, since in the past I've found it to have better support for reading error response bodies. I've only needed to use basic code like this to send same site cross origin cookies to APIs:

    const options = {
         url,
         method,
         headers: {
             accept: 'application/json',
         },
         withCredentials: true,
    };
    const response = await axios.request(options);
    

    SUMMARY

    I can verify that in a compliant same site setup with the right CORS permissions, you can send cookies from a web origin to a sibling API domain, and also from a sibling iframe to the same API. I have done quite a bit of work in this area, and tested your iframe scenario in all of the main browsers. Eg you can run my Online SPA, which uses API driven cookies. As a next step, see if any of this helps, and post back any results if the issue persists.