<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1822615684631785&amp;ev=PageView&amp;noscript=1"/>

How to send a cookie with a cross-origin XMLHttpRequest from a Chrome extension

When developing a Chrome extension, you might need to get an XMLHttpRequest that’s part of a content script to send cookies for a domain when making a request to that domain, if the origin is not that domain. Not much has been written about how to do this.

Dana Woodman, a Chrome extension developer discusses how to do this, but she makes a mistake, claiming that you need to designate the “cookies” permission in your manifest.json. This is not accurate. You can designate the “cookies” permission in manifest.json, but you only need to do that if you want to access cookie data separately from an XmlHttpRequest. Additionally, she makes a mistake that 99% of Chrome extension developers make, assuming that you have to put your domain in the “permissions” field in order to make cross-origin web requests to it.

There are a few Stack Overflow threads like this one and this one that explain the issue, but they also leave out key details and insights.

In this article, I’ll break down exactly what you need to do to pass along cookies to cross-origin XmlHttpRequests in a Chrome extension.

Why would anyone ever want to do this to begin with?

So that your web server endpoint for your Chrome extension can authenticate a user. If part of your Chrome extension setup is to let the user authenticate via a webpage, then you probably set a cookie or a session ID for that authenticated user. If your Chrome extension then makes XHR requests to your web server as part of its functionality, you’ll want to pass cookies along so that you know what user you’re dealing with.

First, let’s clarify the issue of placing “hosts” in the “permissions” field:

Most Chrome extension developers assume that if their website is www.mydomain.com, and their Chrome extension makes XHR requests to www.mydomain.com, then you must put www.mydomain.com in the permissions field of your manifest file. This is simply not true.

I can understand why developers are confused about this though; Google’s documentation on using hosts in the permissions section of manifest.json is poor. The document doesn’t even mention the reason for listing a host, other than to “give access to one or more hosts”. But what does “give access” mean? Furthermore, while the page on match patterns offers a hint at what the purpose of hosts in the permissions field are, clicking on the “host permissions” definition on this page takes you back to the original permissions page with this URL (https://developer.chrome.com/apps/declare_permissions#host-permissions) and unfortunately there is no content tagged with #host-permissions on that page. This is probably an error on Google’s part. SO, we are left to figure out what the purpose of “hosts” in the permissions field ON OUR OWN.

It used to be (prior to Chrome 85) that you could avoid caring about the Access-Control-Allow-Origin header if you placed www.mydomain.com in the “permissions” field of the manifest. It used to be that to make cross origin XHR requests, listing your domain in the permissions field was only needed if the web server for the domain doesn’t already allow cross-origin requests. But now, with Chrome’s new CORS security policy as of Chrome 85, to make any cross-origin XHR request from a content script, the server has to respond with an appropriate Access-Control-Allow-Origin header. So, let’s say you’re making a cross-origin request to www.facebook.com from your content script. Well, now, just having www.facebook.com in the “permissions” field isn’t enough. You would need to ensure that Access-Control-Allow-Origin for www.facebook.com was set to * or your actual content script’s origin. Now, this only applies to content scripts. If you’re making cross origin XHR requests from a background script, then as long as the domain is listed in “permissions”, it doesn’t matter if Access-Control-Allow-Origin isn’t present or isn’t set right.

If the only reason you’re putting your domain in the permissions field is so you can make AJAX XHR requests to it, then don’t do it. Just handle it on the web server by setting the Access-Control-Allow-Origin header. Since the fact that a domain is listed in the “permissions” field of manifest.json no longer means you can make cross-origin requests to it from an extension’s content script, the “permissions” field has essentially become devalued.

There are OTHER reasons you may need a host in the permissions field, EVEN IF the host already has the Access-Control-Allow-Origin header set to *. If you need programatic access to the host’s cookies, and you declare the “cookies” permission in the manifest, then you’ll also need to declare the host in the permissions field. This is only if you need to access the cookies without making an XHR request. The documentation on the cookies permission field states that “To use the cookies API, you must declare the “cookies” permission in your manifest, along with host permissions for any hosts whose cookies you want to access.”

It’s important to understand this because at some point in the evolution of your Chrome extension, you may find that you separate your web server into www.mydomain.com and extension.mydomain.com, so that your marketing website can live at www.mydomain.com and your extension makes calls to extension.mydomain.com. Since Chrome extensions don’t allow you to future-proof your code you may have missed putting extension.mydomain.com in the permissions when you first launched. And if you add it later, your extension will become disabled for everyone until they accept your new permissions, which can be catastrophic to the user base for an extension. So, instead of updating the permissions field, you only need to set your server to allow cross origin requests.

It’s also important to understand this because you don’t want to scare off users when they click the “Install” button and get the permissions warning popup. If you list hosts in “permissions”, the user will be told that you want to have the ability to change the data on those sites. If you don’t, and just handle it via the the web server, you can reduce potentially scary permissions warnings.

Now, let’s talk about sending cookies over the wire with XmlHttpRequests.

Now that you understand what is and isn’t required to make cross origin requests, let’s talk about sending cookies with these requests.

First, you do not need to declare the “cookies” permission in your manifest.json, even though most developers assume you do. Google doesn’t do a good job of explaining what this “cookies” permission is for, but it’s NOT for passing along cookies in XHR requests. And as discussed above, you don’t necessarily need to declare the host in your “permissions” field either.

There are three things you need to make sure of to do this:

  1. Set withCredentials=true in your XMLHttpRequest. Here’s a more detailed explanation of when .withCredentials is necessary. It’s not necessary for non-cross origin requests, so if the XHR call is being made from the same domain as the destination domain of the XHR request, then .withCredentials has no effect. But for a Chrome extension’s content script the “origin” is the “web origin that the content script has been injected into”, and so you’re almost always making a cross-origin request when making an XHR call.
  2. Make sure that the cookie(s) that you want to transmit to the server via the XHR request were originally set with the SameSite=None and Secure attributes. This is a new requirement in 2020. If Chrome has stored 100 cookies for www.mydomain.com and you make an XHR request from mail.google.com to www.mydomain.com, only the cookies out of the 100 that have those two attributes set will be transmitted. You can see exactly what cookies are transmitted by going to the “Cookies” sub-tab in the specific network row in the Network tab. If a cookie is not transmitted, hover over the “i” circular icon for an explanation as to why.
  3. If your code needs access to the XHR request’s response, make sure the AccessControlAllowOrigin header of your server response is NOT set to *. This isn’t allowed. If you do this, then your XHR request set with withCredentials=true will be sent along with all compliant cookies, BUT your content script won’t have access to the response from the XHR request. This error will be logged in the console:Access to XMLHttpRequest at 'https://www.domain.com/ReceiveCookies' from origin 'https://mail.google.com' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.So if your content script code needs access to the web server’s response, AccessControlAllowOrigin needs to be set more specifically to the origin from which the request is being made. In my case, that is https://mail.google.com since my extension works inside Gmail. Now IF you only care about transmitting a request to the server for processing and not reading the server’s response, you actually don’t need to bother with this. For example, you can make a cross-origin XHR request that SETS a cookie even though AccessControlAllowOrigin is *, but your code won’t be able to read a JSON response or any kind of response because access to it will be blocked.What you see in Chrome Dev Tools can be deceptive. In the case where you make an XHR request and set withCredentials=true, but Access-Control-Allow-Origin=*, the item will show as RED in Chrome Dev Tools and the Console will show an error too, BUT in actuality, compliant cookies were transmitted as part of the response, and any cookies set by the server are received in the response and set by the browser. It’s just that because Access-Control-Allow-Origin was *, your JavaScript code can’t read the XHR request’s response.

    Access-Control-Allow-Origin-asterisk
    with credentials false
    Even more confusing is if I set withCredentials=false, then the item is NOT RED, everything looks normal, except the cookies aren’t transmitted, as expected, since that’s the whole point of withCredentials=false. BUT, the response has a Set-Cookie header that is trying to SET COOKIES, and this WILL FAIL, AND NOT SET THE COOKIE, but you’ll be none the wiser because Dev Tools makes it look like everything worked.

    set cookie failed

    Even the Cookies tab on that Network request makes it seem like everything worked:

    network tab red indicator not accurate

    So the lesson is that whether the line in the Network tab is red or not is not an accurate indication of whether a cookie was SET based on the Set-Cookie header in the Response.

If the Network tab in Dev Tools doesn’t show you the cookies…

A recent update to the Chrome Developer Tools made using the Network tab a lot trickier. If you’re using the Network tab to monitor your XHR requests your Chrome extension is making, you’ll often see “Provisional headers shown” for the Request Headers, and you won’t see a Cookie section, so you’ll assume Cookies aren’t being sent. Perhaps you won’t then even try to retrieve the cookie on the server side. It took me a long time to figure this one out. This stack overflow article explains that a recent change to Chrome has made it so that you have to modify a few Chrome flags (chrome://flags) if you want to see the full headers, including any cookies being sent.

Once you make that change, you’ll see that your cookies are being sent.

Bonus #1: How to control the Access-Control-Allow-Origin in IIS 8 and higher

I run everything on IIS, so in order for me to set the header to https://mail.google.com for some calls and * for other calls, I need to:

  1. Install the IIS CORS Module from the Web Platform Installer.
  2. Modify my web.config and add code that looks like this:<cors enabled="true" failUnlistedOrigins="true">
    <add origin="*" />
    <add origin="https://mail.google.com"
    allowCredentials="true"
    maxAge="120"></add>
    <add origin="http://*" allowed="false" />
    </cors>The header is generated based on specificity. So if the origin is actually https://mail.google.com, then AccessControlAllowOrigin will be set to https://mail.google.com. Otherwise, it will be set to *.

Bonus #2: How to set the SameSite and Secure attributes for a cookie in .NET.

This isn’t as easy as it sounds. Microsoft only first introduced support for the SameSite attribute for cookies in .NET 4.7.2, so if you’re using 4.6.1 like me, then you can’t set this property. You could upgrade your entire production environment to 4.7.2, but that could create unforseen side effects without a lot of testing. Fortunately there’s another way. You can simply add the Set-Cookie header to your methods yourself and control exactly how the cookie is set in the browser. Here’s how I do it:

public ActionResult SetExtensionInstallCookie()

{

/*HttpCookie cookie = new HttpCookie("GMassExtensionInstall", "yes");

cookie.Expires = DateTime.UtcNow.AddYears(1);

//cookie.SameSite = SameSiteMode.None;

Response.SetCookie(cookie);*/

HttpContext.Response.Headers.Add("Set-Cookie", "GMassExtensionInstall=yes; expires=Sun, 21-Sep-2025 03:40:10 GMT; path=/; SameSite=None; Secure");

var json = Json(new

{

success = true

}, JsonRequestBehavior.AllowGet);

return json;

}

 

4 Comments
  1. Really really nice explanation!

    There is a down side though with this approach:

    “Access-Control-Allow-Origin’ value can not be ‘*’, cannot be multiple or with wildcard —- it can only be specified as ONE specific origin. That’s a rather significant restriction.

    Still, it is good to have alternative solutions.

    Thanks!

    1. You’re absolutely right. I just updated the article to include how to work around that, and I showed specifically how to deal with that in IIS, which is what I use.

Leave a Reply to gmassblog Cancel reply

Your email address will not be published. Required fields are marked *

Try GMass today

It only takes 30 seconds to install it!

Install Now GMass requires Chrome

GMass

Share This