I have 2 apps in my monorepo. App A is assigned domainA.com and App B is assigned domainB.com
Plan is to open domainB.com in an iframe in domainA.com. All this works locally when the 2 domains are on localhost:3000 and localhost:3001, but once deployed, domainB.com is blocked inside the iframe with the following error on Chrome:
Because your site has the Cross-Origin Embedder Policy (COEP) enabled, each resource must specify a suitable Cross-Origin Resource Policy (CORP). This behavior prevents a document from loading cross-origin resources which don’t explicitly grant permission to be loaded.
To solve this, add the following to the resource’ response header:
Cross-Origin-Resource-Policy: same-site if the resource and your site are served from the same site.
Cross-Origin-Resource-Policy: cross-origin if the resource is served from another location than your website. ⚠️If you set this header, any website can embed this resource.
Alternatively, the document can use the variant: Cross-Origin-Embedder-Policy: credentialless instead of require-corp. It allows loading the resource, despite the missing CORP header, at the cost of requesting it without credentials like Cookies.
When I inspect the request to domainB.com when loaded inside iframe, there are no headers in the response. But when i open domainB.com outside an iframe, the response has all the headers required.
next.config.mjs for domainA:
/** @type {import('next').NextConfig} */
const nextConfig = {
// reactStrictMode: false, // Uncomment to see if duplicate requests are due to React Strict Mode
async headers() {
return [
{
source: "/(.*)",
headers: securityHeaders,
},
];
},
async rewrites() {
return [
{
source: "/home",
destination: "/",
},
];
},
experimental: {
swcPlugins: [
[
"@preact-signals/safe-react/swc",
{
// you should use `auto` mode to track only components which uses `.value` access.
// Can be useful to avoid tracking of server side components
mode: "auto",
} /* plugin options here */,
],
],
},
};
const ContentSecurityPolicy = `
child-src 'self' https://pykernel.app https://www.pykernel.app http://localhost:3001;
frame-src 'self' https://pykernel.app https://www.pykernel.app http://localhost:3001;
`;
const securityHeaders = [
{
key: "Content-Security-Policy",
value: ContentSecurityPolicy.replace(/\n/g, ""),
},
{
key: "Cross-Origin-Embedder-Policy",
value: "credentialless",
},
{
key: "Cross-Origin-Opener-Policy",
value: "same-origin",
},
{
key: "Cross-Origin-Resource-Policy",
value: "cross-origin",
},
];
export default nextConfig;
next.config.mjs for domainB:
/** @type {import('next').NextConfig} */
const nextConfig = {
// reactStrictMode: false, // Uncomment to see if duplicate requests are due to React Strict Mode
pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],
async headers() {
return [
{
source: "/(.*)",
headers: securityHeaders,
},
];
},
};
const ContentSecurityPolicy = `
default-src 'self' vercel.live;
script-src blob: 'self' 'unsafe-eval' 'unsafe-inline' cdn.vercel-insights.com vercel.live va.vercel-scripts.com pyscript.net https://cdn.jsdelivr.net/npm/monaco-editor@0.43.0/ https://cdn.jsdelivr.net/pyodide/ https://cdn.jsdelivr.net/npm/@pyscript/ https://cdn.jsdelivr.net/npm/@micropython/;
worker-src 'self' blob: data:;
style-src * 'self' 'unsafe-inline';
img-src * blob: data:;
media-src 'none';
connect-src * blob:;
font-src * 'self' data:;
frame-src 'self' vercel.live;
frame-ancestors 'self' https://pykernel.com https://www.pykernel.com http://localhost:3000;
`;
const securityHeaders = [
{
key: "Content-Security-Policy",
value: ContentSecurityPolicy.replace(/\n/g, ""),
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "X-DNS-Prefetch-Control",
value: "on",
},
{
key: "X-Frame-Options",
value: "ALLOW-FROM https://pykernel.com https://www.pykernel.com http://localhost:3000",
},
{
key: "Strict-Transport-Security",
value: "max-age=31536000; includeSubDomains; preload",
},
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=()",
},
{
key: "Cross-Origin-Embedder-Policy",
value: "credentialless",
},
{
key: "Cross-Origin-Opener-Policy",
value: "same-origin",
},
{
key: "Cross-Origin-Resource-Policy",
value: "cross-origin",
},
];
export default nextConfig;
why does domainB get the headers in the response when opened by itself and why are these headers missing when opened inside an iframe?
Opened in iframe:
Opened standalone: