TABLE OF CONTENTS
Set Cookies On Localhost
Author Sagi Liba
Sagi Liba on Jan 16, 2022
12 min 🕐

Lately, I've had to run a React application inside an IFrame, where the parent application is Angular basedand does all the authentication for me.

The Angular application after signing in a user receives a set of cookies used to authenticate the logged-in user. When I've tried to run those applications together locally in order to debug a live dev/stage/prod environment I saw that the cookies were not being saved, meaning I'm effectively "logged out".

This article is part of the series: Debugging Remote APIs Locally.

It is about all the challenges I've faced, and the solutions I've found to make sure you can run your frontend locally against any remote API.

Previous articles:

Assumptions

The information this article relies on is based on the previous articles of the series, where I've used a proxy to bypass CORS, and how I've set my local environment to use HTTPS during development.

This article is highly technical, if you are new to CORS or setting HTTPS, please follow the articles I've mentioned above before you continue.☝️

The following are the conditions that allowed me to set cookies on localhost:

  • You work against a remote / deployed API.
    • I have not tested a local frontend and backend that set cookies.
  • You don't have any CORS issues.
    • You will need to send your requests through a local proxy, the code sample will be based on the previous article.
  • Your server uses the headerAccess-Control-Allow-Credentialsset to true.
    • This header allows cookies to be passed to another origin.
  • You send your request usingwithCredentialsset to true when expecting cookies.
    • You can set that usingaxios, andfetchhas a similiar functionality.
  • I worked against an API using HTTPS, therefore, I don't know if it works with an HTTP API.
    • You can use HTTPS for local development, learn to set it up here.

The Problem

Setting cookies on localhost is hard, it takes a lot of knowledge about different edge cases, and it took me a while to figure it out.

To tackle this issue I've started to search for ways that you can set cookies on your localhost domain, but sometimes Google can fail you, I've found answers ranging from its not possible to partially correct, but there weren't any home runs.

Without any solid information, I've decided to find a way to solve this myself.

As far as my tests go, I've checked that all of my projects are using HTTPS:

  • My local React project is using HTTPS (This app is being served using an IFrame).
  • My local Angular project is using HTTPS (Deals with authentication and serves the IFrame of the React app)
  • My remote API is using HTTPS.

The Solution

Ok, enough of the prerequisites, let's set our cookie!

I've created a new React project usingcreate-react-app, and I've made sure to run it using HTTPS in development, you need to do the same based on the previous articles I've mentioned.

When the application is mounted I'm sending a request to our local proxy, which we are about to create on port 5050.

Copy
function App() {
useEffect(() => {
(async () => {
// I've used Axios for the request, you can use whatever you wish.
const request = await axios
.get("https://localhost:5050/getCookie", { withCredentials: true })
.then((res) => res.data);
})();
}, []);
return <div className="App"></div>;
}
export default App;

Notice how I've made sure to setwithCredentialsto true, meaning I am knowingly expecting a cookie to be set and allowing it.

Now, I'm creating a proxy that will run on port 5050, and will direct my requests to my remote API.

The code inside is the same as the code in the "Bypass CORS by Using A Proxy" article, I've only used HTTPS this time, and the proxy certificates are the same as the React project (key.pem & cert.pem for localhost).

Copy
const options = {
// /getCookie will be sent to: https://target-api-server.com/api/getCookie
target: "https://target-api-server.com/api",
port: 5050,
};
const fs = require("fs");
const http = require("http");
const https = require("https");
const httpProxy = require("http-proxy");
const proxy = httpProxy.createProxyServer({});
var https_options = {
key: fs.readFileSync(__dirname + "/key.pem"),
cert: fs.readFileSync(__dirname + "/cert.pem"),
};
const ignoreAccessControlHeaders = (header) =>
!header.toLowerCase().startsWith("access-control-");
// Received a response from the target
proxy.on("proxyRes", (proxyRes, req, res) => {
proxyRes.headers = Object.keys(proxyRes.headers)
.filter(ignoreAccessControlHeaders)
// Create an object with all the relevant headers
.reduce(
(all, header) => ({ ...all, [header]: proxyRes.headers[header] }),
{}
);
// Override the response Access-Control-X headers
if (req.headers["access-control-request-method"]) {
res.setHeader(
"access-control-allow-methods",
req.headers["access-control-request-method"]
);
}
if (req.headers["access-control-request-headers"]) {
res.setHeader(
"access-control-allow-headers",
req.headers["access-control-request-headers"]
);
}
if (req.headers.origin) {
res.setHeader("access-control-allow-origin", req.headers.origin);
res.setHeader("access-control-allow-credentials", "true");
}
});
// Failed to send a request to the target
proxy.on("error", (error, req, res) => {
res.writeHead(500, {
"Content-Type": "text/plain",
});
res.end("Proxy Error: " + error);
});
//HTTPS
var server = https.createServer(https_options, function (req, res) {
proxy.web(req, res, {
target: options.target,
secure: true, // Verify the SSL Certs
changeOrigin: true, // Set origin of the host header to the target URL
});
});
console.log("listening on port", options.port);
server.listen(options.port);

Now the server must return a cookie, I've used AWS API Gateway to create my remote API, and created an endpoint namedgetCookiethat will return the following cookie header:

Copy
.
.
"Set-Cookie": "cookieKey=cookieValue; Path=/; SameSite=None; Secure; Domain=localhost; Expires=Sun, 24 Apr 2022 09:39:48 GMT; "
.
.

To allow the cookies to be passed to a different origin I've set:

  • SameSite=None
  • Secure, it's a requirement when SameSite is none.

All that is left is to run our projects and see if it works, you need to run:

  • React application that runs using HTTPS.
  • A proxy that is set to your remote API, using HTTPS.
  • Make sure your remote API endpoint is working and being served using HTTPS.

I've ran my React application and entered it through:

Copy
https://localhost:3000

Notice the lock sign, indicating a secure connection:

running-react-using-https

The GET request for /getCookie, is fired and goes to our local proxy at:

Copy
https://localhost:5050/getCookie

The request is forwarded to our remote API on AWS API Gateway, and in return, we have received the cookie I've talked about earlier:

Copy
.
.
"Set-Cookie": "cookieKey=cookieValue; Path=/; SameSite=None; Secure; Domain=localhost; Expires=Sun, 24 Apr 2022 09:39:48 GMT; "
.
.

cookie-header

I've then checked to see that theCookies tabshows the same cookie information again, making sure there are no errors, such as a wrong domain, I'll be talking about it soon:

network-cookies

Finally, I've checked to see if it was set inside our Applications tabs:

applications-cookies

The cookie was successfully set and is being sent with subsequent requests.

Proxy Super Powers

Because we are using a proxy we can edit the response from the server, and actually edit the cookie headers being sent. This enables us to override any restrictions set by the server as to the Domain of the cookies, Path, SameSite, etc...

Here are a few examples of overriding the cookie's values:

(It's an addition to our proxy code)

Copy
proxy.on("proxyRes", (proxyRes, req, res) => {
// Edit set-cookie header
if ( proxyRes.headers['set-cookie'] ) {
proxyRes.headers["set-cookie"] = proxyRes.headers["set-cookie"].map(cookie => {
// Disabling SameSite Strict
// You might use None intead of Lax.
cookie = cookie.replaceAll("SameSite=Strict","SameSite=Lax");
// Disabling any Domain being set
cookie = cookie.replaceAll("Domain=ireadyoulearn.info;","");
// Changing the cookies path
cookie = cookie.replaceAll("Path=/login/token","Path=/");
return cookie;
})
}
// Then the rest of the "proxyRes" code you already know...
}

Override the cookies as your needs require, do anything you need to make sure it works for your specific use case.

Localhost Cookies Are Shared

When setting a cookie on localhost it is important to understand that it is set directly on the domain "localhost", without any port specification, meaning if one of your applications has set cookies on localhost, it will also be available on other applications running there as well.

Note:

  • When multiple projects set cookies on localhost, they might override one another when using the same keys for your cookies.

Debugging Cookies

I've ran into a weird situation when I was trying to set the cookie, I call it "The Case Of The Missing Cookie".

I was trying to figure out if the proxy is even needed, I've done so by sending the request /getCookieto the remote API directly,without using a proxy.

Copy
const request = await axios
.get("https://target-api-server.com/api/getCookie", { withCredentials: true })
.then((res) => res.data);

The cookie's value was the same as before:

Copy
.
.
"Set-Cookie": "cookieKey=cookieValue; Path=/; SameSite=None; Secure; Domain=localhost; Expires=Sun, 24 Apr 2022 09:39:48 GMT; "
.
.

I can see that it is being sent with subsequent requests:

subsequent-cookie

But the applications tab ( Devtools -> Applications -> Storage -> Cookies ) is showing that there are no cookies:

no-cookies

Let's just say it took me a while to find an explanation for this situation, StackOverflow is full of these kinds of situations with no valid answers, but after a while it struck me,the domain is incorrect.

So why is this happening? As we just saw, the cookie'sDomainis set correctly tolocalhost which makes us believe everything should be working correctly, but when we check the Cookies tab we can see that the domain is different:

(It points to AWS, instead of localhost as expected) wrong-cookie-domain

The only way to see a cookie in the Applications tab is to be in the correct domain it was set to (and the correct path), meaning we have to access our remote API and only then we will see our missing cookie.

Finally, I've needed to find a way to set the correct cookie domain, the only way I've found that changed this property to "localhost" was to change the frontend request URL I'm sending my cookie to:

Copy
...
https://my-api.amazonaws.com/api/getCookie
->
https://localhost:5050/getCookie
...

And that's why I chose to use a proxy in here as well in order to make sure the cookie is being set to the correct domain.

So what does this teach us?
  • Always use the Cookies tab to debug your cookies, it will hold the correct information.
  • The domain the cookie is set too is where you will need to go to find your cookies and view them in the applications tab.
  • Using a proxy has multiple benefits and in this case it's crucial to set our cookies.

A Final Note:

It is possible to omit the Domain property and let the browser infer the origin by itself, you have to make sure you know how to debug the cookie (as explained above) when it won't work as expected.

*  *  *

This is it, it was hard to solve and hard to write about. This is the final article of the seriesDebug Remote APIs Locally, I hope you are now able to bypass CORS headers, run your development application using HTTPS, better understand cookies, and be able to set them on your localhost.

© 2020-present Sagi Liba. All Rights Reserved