When working with an application that integrates with external applications and APIs on different domains, maintaining a shared state, such as authentication, can be challenging due to browser restrictions on cross-domain cookies. While data can be shared using browser local/session storage and cookies can be set client-side, these approaches increase the risk of XSS attacks. Using Secure and HttpOnly cookies provides more security, as these cookies can only be managed through the backend and are seamlessly sent on every same-origin request. This post discusses the approach to making cookies set by an API running on a different domain available to the application’s domain in the browser.
Scenario:
The application runs on https://test.mydomain.com:8080
invokes an API at https://test.api.com:8081
from the client side. The API sets a cookie that should be available for test.mydomain.com
. Additionally, all subsequent requests to https://test.mydomain.com
or any subdomains on mydomain.com should include the cookie to maintain the state.
Problem:
In a cross-domain context, the request to the https://test.api.com:8081
API service cannot set the cookie for test.mydomain.com
or the root domain.mydomain.com
due to browser cross-domain restrictions. The browser rejects the cookie in this scenario.
Solution:
To solve this problem, proxy the API calls through the same domain. This approach ensures that the cookies are correctly handled and included in all requests. Here are the steps to achieve this:
Proxy API Calls Through the Same Domain:
- Use a CDN: If a CDN is used, configure it to send the
/api
calls to the API servers. - Use Proxy Servers: Use proxy servers like Apache or Nginx to handle API requests.
- Server Handling: The server handling
https://test.mydomain.com
requests can proxy the API requests tohttps://test.api.com
.
Setting the Cookie Domain:
- The API can avoid setting the cookie domain explicitly, allowing the browser to use the current domain (
test.mydomain.com
). - Alternatively, the API can set the cookie directly on
test.mydomain.com
or.mydomain.com
. This ensures the cookie is available fortest.mydomain.com
and all subdomains undermydomain.com
.
Dynamic Cookie Setting:
- If the cookie needs to be set dynamically based on the requesting domain, use the
Referer
header to determine the domain and set the cookie accordingly.
Proxy Rewriting:
- If you do not have control over the API to set the cookies on your original domain, configure the proxy to rewrite the cookie domain of the API response to the actual domain before sending the response to the client.
Forwarding Cookies:
- In a forward flow, forward the cookie to the API domain. Most proxies support this.
Implementation Example:
Here’s a demonstration using two Express servers. The server.js
handles all requests on https://test.mydomain.com:8080
and proxies API requests to api-server.js
running on https://test.api.com:8081
.
server.js:
const express = require("express");
const path = require("path");
const https = require("https");
const fs = require("fs");
const { createProxyMiddleware } = require("http-proxy-middleware");
const app = express();
const port = 8080;
// Load SSL key and certificate
const key = fs.readFileSync("SSL/server.key");
const cert = fs.readFileSync("SSL/server.crt");
const options = { key, cert };
// Serve static files from the "public" directory
app.use(express.static(path.join(__dirname, "public")));
// Ignore self-signed certificate errors (development only)
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
//https://test.mydomain.com:8080/api
// Proxy setup to forward API requests to the API server
app.use(
"/api",
createProxyMiddleware({
//target: "https://test.api.com:8081",
target: "https://test.synvie.de:8081",
changeOrigin: true,
pathRewrite: { "^/api": "" },
secure: false, // Allow self-signed certificates
/** on:{
proxyRes: (proxyRes, req, res) => {
console.log('Inside onProxyRes...'); // Log original cookies
const cookies = proxyRes.headers['set-cookie'];
if (cookies) {
console.log('Original cookies:', cookies); // Log original cookies
proxyRes.headers['Set-Cookie'] = cookies.map(cookie => {
const newCookie = cookie.replace(/Domain=[^;]+;/i, 'Domain=test.mydomain.com;');
console.log('Modified cookie:', newCookie); // Log modified cookie
return newCookie;
});
}
}
},**/ // if the target service can not set domain in appropriate client side root domain, the cookie need to be rewritten to the clinet side root domain from backend API domain using CDN(if supported) or another proxy server like Apache
onError: (err, req, res) => {
console.error("Proxy error:", err);
res.status(500).send("Proxy error occurred");
},
})
);
// Endpoint to check for cookies
//https://test.mydomain.com:8080/check
app.get("/check", (req, res) => {
const cookie = req.headers.cookie || "No cookies found";
console.log("Cookies received:", cookie); // Add logging
res.send(`Cookies received: ${cookie}`);
});
// Add error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send("Something broke!");
});
// Create HTTPS server
https.createServer(options, app).listen(port, () => {
console.log(`Client server running at https://test.mydomain.com:${port}`);
});
api-server.js:
const express = require("express");
const cors = require("cors");
const https = require("https");
const fs = require("fs");
const app = express();
const port = 8081;
// Load SSL key and certificate
const key = fs.readFileSync("SSL/server.key");
const cert = fs.readFileSync("SSL/server.crt");
const options = { key, cert };
// CORS configuration to allow requests from your client domain
app.use(
cors({
origin: "https://test.mydomain.com:8080",
credentials: true,
})
);
//https://test.api.com:8081
app.get("/", (req, res) => {
console.log("Server Name:" + req.hostname);
console.log("Referrer:" + req.get("Referer"));
// Get the referer header
const referer = req.get("Referer");
if (referer) {
// Extract the root domain from the referer
const url = new URL(referer);
const domain = url.hostname.split(".").slice(-2).join("."); // Get the root domain (e.g., mydomain.com)
console.log("Cookie Domain:" + domain);
// Set the cookie with the dynamic domain
res.cookie("testCookie", "cookieValue", {
httpOnly: true,
secure: true, // Set to true for HTTPS
sameSite: "Strict", // Required for cross-site cookies
domain: `.${domain}`, // Set the domain dynamically
});
res.send(`HttpOnly cookie has been set for domain: .${domain}`);
} else {
res.status(400).send("Referer header not found");
}
});
// Add error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send("Something broke!");
});
// Create HTTPS server
https.createServer(options, app).listen(port, () => {
console.log(`API server running at https://test.api.com:${port}`);
});
Index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cross-Domain Cookie Test</title>
</head>
<body>
<h1>Cross-Domain Cookie Test</h1>
<button id="invoke-button">Invoke API</button>
<button id="check-cookie-button">Check Cookie</button>
<p id="result"></p>
<script>
document.getElementById('invoke-button').addEventListener('click', async () => {
try {
const response = await fetch('/api/', {
method: 'GET',
credentials: 'include' // Important to include credentials
});
const data = await response.text();
console.log('Fetch Cookie response:', data); // Add logging
document.getElementById('result').innerText = `Response: ${data}`;
} catch (error) {
console.error('Fetch Cookie error:', error);
document.getElementById('result').innerText = `Error: ${error.message}`;
}
});
document.getElementById('check-cookie-button').addEventListener('click', async () => {
try {
const response = await fetch('/check', {
method: 'GET',
credentials: 'include' // Important to include credentials
});
const data = await response.text();
console.log('Check Cookie response:', data); // Add logging
document.getElementById('result').innerText = `Response: ${data}`;
} catch (error) {
console.error('Check Cookie error:', error);
document.getElementById('result').innerText = `Error: ${error.message}`;
}
});
</script>
</body>
</html>
Setup Instructions:
Install dependencies and start the servers:
cd Cross-Domain-Cookies
npm install
node server.js
node api-server.js
Access the application:
Navigate to https://test.mydomain.com:8080
. The cookie from the API will be set on .mydomain.com
. For your local testing g put host entry in your host file for test.mydomain.com and test.api.com pointing 127.0.0.1.
For local testing, add the following entries to your hosts file to point test.mydomain.com
and test.api.com
to 127.0.0.1
:
127.0.0.1 test.mydomain.com
127.0.0.1 test.api.com
Conclusion:
By following these steps, you can effectively manage cookies across different domains, ensuring a consistent state across your web applications. This approach is useful when integrating with various applications and ensuring that users always see the same domain and have access to the same cookies. For sample code, refer to Cross-Domain Cookies.