fix: Add CORP and COEP headers to embedded pages (#15275)

* Add CORP headers

* Improve test

* Add unit tests as well

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
This commit is contained in:
Hariom Balhara
2024-07-29 11:54:31 +02:00
committed by GitHub
co-authored by Keith Williams
parent 6e66f7f8f8
commit 37caa57798
12 changed files with 302 additions and 21 deletions
+4 -1
View File
@@ -14,7 +14,10 @@ export default function withEmbedSsrAppDir<T extends Record<string, any>>(
) {
return async (context: GetServerSidePropsContext): Promise<T> => {
const { embed, layout } = context.query;
const isCOEPEnabled = context.query["flag.coep"] === "true";
if (isCOEPEnabled) {
context.res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
}
try {
const props = await getData(context);
+132
View File
@@ -133,6 +133,24 @@ describe("withEmbedSsr", () => {
},
});
});
it('should add Cross-Origin-Embedder-Policy header when "flag.coep" query param is true', async () => {
const withEmbedGetSsr = withEmbedSsr(
getServerSidePropsFnGenerator({
redirectUrl: "/reschedule",
})
);
const context = getServerSidePropsContextArg({
embedRelatedParams: {
layout: "week_view",
embed: "",
"flag.coep": "true",
},
});
await withEmbedGetSsr(context);
expect(context.res.getHeader("Cross-Origin-Embedder-Policy")).toBe("require-corp");
});
});
describe("when redirect destination is absolute, should add /embed to end of the path", () => {
@@ -206,6 +224,23 @@ describe("withEmbedSsr", () => {
},
});
});
it('should add Cross-Origin-Embedder-Policy header when "flag.coep" query param is true', async () => {
const withEmbedGetSsr = withEmbedSsr(
getServerSidePropsFnGenerator({
redirectUrl: "https://calcom.cal.local/owner",
})
);
const context = getServerSidePropsContextArg({
embedRelatedParams: {
layout: "week_view",
embed: "",
"flag.coep": "true",
},
});
await withEmbedGetSsr(context);
expect(context.res.getHeader("Cross-Origin-Embedder-Policy")).toBe("require-corp");
});
});
});
@@ -233,6 +268,85 @@ describe("withEmbedSsr", () => {
},
});
});
it('should add Cross-Origin-Embedder-Policy header when "flag.coep" query param is true', async () => {
const withEmbedGetSsr = withEmbedSsr(
getServerSidePropsFnGenerator({
props: {
prop1: "value1",
},
})
);
const context = getServerSidePropsContextArg({
embedRelatedParams: {
layout: "week_view",
embed: "",
"flag.coep": "true",
},
});
await withEmbedGetSsr(context);
expect(context.res.getHeader("Cross-Origin-Embedder-Policy")).toBe("require-corp");
});
it('should not add Cross-Origin-Embedder-Policy header when "flag.coep" query param is false', async () => {
const withEmbedGetSsr = withEmbedSsr(
getServerSidePropsFnGenerator({
props: {
prop1: "value1",
},
})
);
const context = getServerSidePropsContextArg({
embedRelatedParams: {
layout: "week_view",
embed: "",
"flag.coep": "false",
},
});
await withEmbedGetSsr(context);
expect(context.res.getHeader("Cross-Origin-Embedder-Policy")).toBeUndefined();
});
it('should not add Cross-Origin-Embedder-Policy header when "flag.coep" query param is not set', async () => {
const withEmbedGetSsr = withEmbedSsr(
getServerSidePropsFnGenerator({
props: {
prop1: "value1",
},
})
);
const context = getServerSidePropsContextArg({
embedRelatedParams: {
layout: "week_view",
embed: "",
},
});
await withEmbedGetSsr(context);
expect(context.res.getHeader("Cross-Origin-Embedder-Policy")).toBeUndefined();
});
it('should not add Cross-Origin-Embedder-Policy header when "flag.coep" query param is set to something other than true', async () => {
const withEmbedGetSsr = withEmbedSsr(
getServerSidePropsFnGenerator({
props: {
prop1: "value1",
},
})
);
const context = getServerSidePropsContextArg({
embedRelatedParams: {
layout: "week_view",
embed: "",
"flag.coep": "abasdf",
},
});
await withEmbedGetSsr(context);
expect(context.res.getHeader("Cross-Origin-Embedder-Policy")).toBeUndefined();
});
});
describe("when gSSP doesn't have props or redirect ", () => {
@@ -254,5 +368,23 @@ describe("withEmbedSsr", () => {
expect(ret).toEqual({ notFound: true });
});
it('should add Cross-Origin-Embedder-Policy header when "flag.coep" query param is true', async () => {
const withEmbedGetSsr = withEmbedSsr(
getServerSidePropsFnGenerator({
notFound: true,
})
);
const context = getServerSidePropsContextArg({
embedRelatedParams: {
layout: "week_view",
embed: "",
"flag.coep": "true",
},
});
await withEmbedGetSsr(context);
expect(context.res.getHeader("Cross-Origin-Embedder-Policy")).toBe("require-corp");
});
});
});
+5
View File
@@ -11,6 +11,10 @@ export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
const ssrResponse = await getServerSideProps(context);
const embed = context.query.embed;
const layout = context.query.layout;
const isCOEPEnabled = context.query["flag.coep"] === "true";
if (isCOEPEnabled) {
context.res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
}
if ("redirect" in ssrResponse) {
const destinationUrl = ssrResponse.redirect.destination;
@@ -44,6 +48,7 @@ export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
if (!("props" in ssrResponse)) {
return ssrResponse;
}
return {
...ssrResponse,
props: {
+27
View File
@@ -387,6 +387,13 @@ const nextConfig = {
};
},
async headers() {
// This header can be set safely as it ensures the browser will load the resources even when COEP is set.
// But this header must be set only on those resources that are safe to be loaded in a cross-origin context e.g. all embeddable pages's resources
const CORP_CROSS_ORIGIN_HEADER = {
key: "Cross-Origin-Resource-Policy",
value: "cross-origin",
};
return [
{
source: "/auth/:path*",
@@ -419,6 +426,26 @@ const nextConfig = {
},
],
},
{
source: "/embed/embed.js",
headers: [CORP_CROSS_ORIGIN_HEADER],
},
{
source: "/:path*/embed",
// COEP require-corp header is set conditionally when flag.coep is set to true
headers: [CORP_CROSS_ORIGIN_HEADER],
},
// These resources loads through embed as well, so they need to have CORP_CROSS_ORIGIN_HEADER
...[
{
source: "/api/avatar/:path*",
headers: [CORP_CROSS_ORIGIN_HEADER],
},
{
source: "/avatar.svg",
headers: [CORP_CROSS_ORIGIN_HEADER],
},
],
...(isOrganizationsEnabled
? [
{
+5 -1
View File
@@ -20,7 +20,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const { uid: bookingUid, seatReferenceUid } = z
.object({ uid: z.string(), seatReferenceUid: z.string().optional() })
.parse(context.query);
const coepFlag = context.query["flag.coep"];
const { uid, seatReferenceUid: maybeSeatReferenceUid } = await maybeGetBookingUidFromSeat(
prisma,
bookingUid
@@ -136,6 +136,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
destinationUrl.set("rescheduleUid", seatReferenceUid || bookingUid);
// TODO: I think we should just forward all the query params here including coep flag
if (coepFlag) {
destinationUrl.set("flag.coep", coepFlag as string);
}
return {
redirect: {
destination: `/${eventPage}?${destinationUrl.toString()}${
@@ -58,6 +58,9 @@ export const getServerSideProps = async function getServerSideProps(
const { currentOrgDomain } = orgDomainConfig(context.req);
const isEmbed = params.appPages[1] === "embed";
if (context.query["flag.coep"] === "true") {
context.res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
}
const form = await prisma.app_RoutingForms_Form.findFirst({
where: {
+44 -16
View File
@@ -36,6 +36,9 @@
Cal.ns.withoutReloadNamespace("inline", {
elementOrSelector: selector,
calLink: "pro?case=addInlineEmbedInANewNamespaceWithoutReload",
config: {
"flag.coep":"true"
}
});
};
</script>
@@ -88,6 +91,9 @@
Cal.ns.doubleInstall("inline", {
elementOrSelector: selector,
calLink: "pro?case=doubleInstallSnippetWithInlineEmbedWithNonDefaultNamespace",
config: {
"flag.coep":"true"
}
});
embedSnippet();
Cal("init", "doubleInstall", {
@@ -97,6 +103,9 @@
Cal.ns.doubleInstall("inline", {
elementOrSelector: selector,
calLink: "pro",
config: {
"flag.coep":"true"
}
});
}
@@ -109,6 +118,9 @@
Cal("inline", {
elementOrSelector: selector,
calLink: "pro?case=doubleInstallSnippetWithInlineEmbed",
config: {
"flag.coep":"true"
}
});
embedSnippet();
Cal("init", {
@@ -118,6 +130,9 @@
Cal("inline", {
elementOrSelector: selector,
calLink: "pro",
config: {
"flag.coep":"true"
}
});
}
@@ -133,10 +148,16 @@
Cal.ns['namespace-init-test-2']("inline", {
elementOrSelector: '#two-different-namespace-with-different-init-config-content-2',
calLink: "pro",
config: {
"flag.coep":"true"
}
});
Cal.ns['namespace-init-test-1']("inline", {
elementOrSelector: '#two-different-namespace-with-different-init-config-content-1',
calLink: "pro",
config: {
"flag.coep":"true"
}
});
}
</script>
@@ -198,7 +219,7 @@
<script>
if (only === "all" || only === "prerender-test") {
document.write(`
<button data-cal-namespace="e2ePrerenderLightTheme" data-cal-config='{"theme":"dark", "email":"preloaded-prefilled@example.com", "name": "Preloaded Prefilled"}' data-cal-link="free/30min">Book with Free User[Dark Theme]</button>
<button data-cal-namespace="e2ePrerenderLightTheme" data-cal-config='{"theme":"dark", "email":"preloaded-prefilled@example.com", "name": "Preloaded Prefilled", "flag.coep": "true"}' data-cal-link="free/30min">Book with Free User[Dark Theme]</button>
<i
>Corresponding Cal Link is being prerendered. Assuming that it would take you some time to click this
as you are reading this text, it would open up super fast[If you are running a production build on
@@ -208,7 +229,7 @@
}
if (only === "all" || only === "preload-test") {
document.write(`
<button data-cal-namespace="preloadTest" data-cal-config='{"theme":"dark", "email":"preloaded-prefilled@example.com", "name": "Preloaded Prefilled"}' data-cal-link="free/30min">Book with Free User[Dark Theme]</button>
<button data-cal-namespace="preloadTest" data-cal-config='{"theme":"dark", "email":"preloaded-prefilled@example.com", "name": "Preloaded Prefilled", "flag.coep": "true"}' data-cal-link="free/30min">Book with Free User[Dark Theme]</button>
<i
>Corresponding Cal Link is being preloaded. That means that all the resources would be preloaded. This could be useful in preloading possible resources if you don't know before hand which type of embed you want to show</i
>`);
@@ -222,58 +243,58 @@
<a href="?only=ns:floatingButton">Floating Popup</a>
<h2>Popup Examples</h2>
<button data-cal-namespace="e2ePopupLightTheme" data-cal-link="free" data-cal-config='{"theme":"light"}'>Book an event with Free[Light Theme]</button>
<button data-cal-namespace="popupAutoTheme" data-cal-link="free">
<button data-cal-namespace="e2ePopupLightTheme" data-cal-link="free" data-cal-config='{"theme":"light", "flag.coep": "true"}'>Book an event with Free[Light Theme]</button>
<button data-cal-namespace="popupAutoTheme" data-cal-link="free" data-cal-config='{"flag.coep": "true"}'>
Book with Free User[Auto Theme]
</button>
<button data-cal-namespace="popupDarkTheme" data-cal-config='{"theme":"dark"}' data-cal-link="free">
<button data-cal-namespace="popupDarkTheme" data-cal-config='{"theme":"dark", "flag.coep": "true"}' data-cal-link="free">
Book with Free User[Dark Theme]
</button>
<button
data-cal-namespace="popupTeamLinkLightTheme"
data-cal-config='{"theme":"light"}'
data-cal-config='{"theme":"light", "flag.coep": "true"}'
data-cal-link="team/seeded-team/collective-seeded-team-event">
Book with Test Team[Light Theme]
</button>
<button
data-cal-namespace="popupTeamLinkDarkTheme"
data-cal-config='{"theme":"dark"}'
data-cal-config='{"theme":"dark", "flag.coep": "true"}'
data-cal-link="team/seeded-team/collective-seeded-team-event">
Book with Test Team[Dark Theme]
</button>
<button data-cal-namespace="popupTeamLinksList" data-cal-link="team/seeded-team/">
<button data-cal-namespace="popupTeamLinksList" data-cal-link="team/seeded-team/" data-cal-config='{"flag.coep": "true"}'>
See Team Links [Auto Theme]
</button>
<script>
let popupRescheduleId =
new URL(document.URL).searchParams.get("popupRescheduleId") || "qm3kwt3aTnVD7vmP9tiT2f";
document.write(
`<button data-cal-namespace="popupReschedule" data-cal-link="reschedule/${popupRescheduleId}">Reschedule Event[Auto Theme]</button>`
`<button data-cal-namespace="popupReschedule" data-cal-config='{"flag.coep":"true"}' data-cal-link="reschedule/${popupRescheduleId}">Reschedule Event[Auto Theme]</button>`
);
</script>
<button data-cal-namespace="popupPaidEvent" data-cal-link="pro/paid">
<button data-cal-namespace="popupPaidEvent" data-cal-link="pro/paid" data-cal-config='{"flag.coep": "true"}'>
Book Paid Event [Auto Theme]
</button>
<button data-cal-namespace="popupHideEventTypeDetails" data-cal-link="free/30min">
<button data-cal-namespace="popupHideEventTypeDetails" data-cal-link="free/30min" data-cal-config='{"flag.coep": "true"}'>
Book Free Event [Auto Theme][uiConfig.hideEventTypeDetails=true]
</button>
<button data-cal-namespace="routingFormAuto" data-cal-link="forms/948ae412-d995-4865-875a-48302588de03">
<button data-cal-namespace="routingFormAuto" data-cal-link="forms/948ae412-d995-4865-875a-48302588de03" data-cal-config='{"flag.coep": "true"}'>
Book through Routing Form [Auto Theme] - Test for 'routed' event
</button>
<button
data-cal-namespace="routingFormDark"
data-cal-config='{"theme":"dark"}'
data-cal-config='{"theme":"dark", "flag.coep": "true"}'
data-cal-link="forms/948ae412-d995-4865-875a-48302588de03">
Book through Routing Form [Dark Theme]
</button>
<button data-cal-namespace="popupPaidEvent" data-cal-config='{"layout":"week_view"}' data-cal-link="pro/paid">
<button data-cal-namespace="popupPaidEvent" data-cal-config='{"layout":"week_view", "flag.coep": "true"}' data-cal-link="pro/paid">
Book Paid Event - weekly view
</button>
<button data-cal-namespace="popupPaidEvent" data-cal-config='{"layout":"column_view"}' data-cal-link="pro/paid">
<button data-cal-namespace="popupPaidEvent" data-cal-config='{"layout":"column_view", "flag.coep": "true"}' data-cal-link="pro/paid">
Book Paid Event - column view
</button>
<a style="display: block;" data-cal-namespace="childElementTarget" href="javascript:void(0)" data-cal-link="free/30min">
<a style="display: block;" data-cal-namespace="childElementTarget" href="javascript:void(0)" data-cal-link="free/30min" data-cal-config='{"flag.coep": "true"}'>
I am Anchor
<b>[I am Bold inside anchor]</b>
<span>
@@ -350,6 +371,13 @@
<div>If you render booking embed in me, I would not let you be more than 30% wide</div>
</div>
</div>
<div class="inline-embed-container" id="cal-booking-place-corpTest">
<h3><a href="?only=ns:corpTest">CORP/COEP Blocking Test</a></h3>
<div class="place">
<div>It would forever load</div>
</div>
</div>
</div>
<div class="inline-embed-container" id="cal-booking-place-fifth">
+34 -1
View File
@@ -58,6 +58,7 @@ if (only === "all" || only === "ns:default") {
notes: "Test Meeting",
guests: ["janedoe@example.com", "test@example.com"],
theme: "dark",
"flag.coep": "true",
},
});
Cal("on", {
@@ -84,6 +85,7 @@ if (only === "all" || only === "ns:second") {
id: "cal-booking-place-second-iframe",
},
theme: "auto",
"flag.coep": "true",
},
}
);
@@ -111,6 +113,7 @@ if (only === "all" || only === "ns:third") {
iframeAttrs: {
id: "cal-booking-place-third-iframe",
},
"flag.coep": "true",
},
},
],
@@ -161,6 +164,7 @@ if (only === "all" || only === "ns:fourth") {
iframeAttrs: {
id: "cal-booking-place-fourth-iframe",
},
"flag.coep": "true",
},
},
],
@@ -188,6 +192,28 @@ if (only === "all" || only === "ns:fourth") {
callback,
});
}
if (only === "all" || only === "ns:corpTest") {
Cal("init", "corpTest", {
debug: true,
origin: "http://localhost:3000",
});
Cal.ns.corpTest([
"inline",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
{
elementOrSelector: "#cal-booking-place-corpTest .place",
calLink: "pro",
config: {
iframeAttrs: {
id: "cal-booking-place-corpTest-iframe",
},
"flag.coep": "false",
},
},
]);
}
if (only === "all" || only === "ns:fifth") {
Cal("init", "fifth", {
debug: true,
@@ -204,6 +230,7 @@ if (only === "all" || only === "ns:fifth") {
iframeAttrs: {
id: "cal-booking-place-fifth-iframe",
},
"flag.coep": "true",
},
},
]);
@@ -219,7 +246,7 @@ if (only === "all" || only === "prerender-test") {
origin: origin,
});
Cal.ns.e2ePrerenderLightTheme("prerender", {
calLink: "free/30min",
calLink: "free/30min?flag.coep=true",
type: "modal",
});
}
@@ -250,6 +277,7 @@ if (only === "all" || only === "inline-routing-form") {
iframeAttrs: {
id: "cal-booking-place-inline-routing-form-iframe",
},
"flag.coep": "true",
},
},
]);
@@ -274,6 +302,7 @@ if (only === "all" || only === "hideEventTypeDetails") {
iframeAttrs: {
id: `cal-booking-place-${identifier}-iframe`,
},
"flag.coep": "true",
},
},
],
@@ -404,6 +433,7 @@ if (only === "all" || only == "ns:floatingButton") {
iframeAttrs: {
id: "floatingtest",
},
"flag.coep": "true",
name: "John",
email: "johndoe@gmail.com",
notes: "Test Meeting",
@@ -431,6 +461,7 @@ if (only === "all" || only == "ns:monthView") {
iframeAttrs: {
id: "cal-booking-place-monthView-iframe",
},
"flag.coep": "true",
layout: "month_view",
},
}
@@ -459,6 +490,7 @@ if (only === "all" || only == "ns:weekView") {
iframeAttrs: {
id: "cal-booking-place-weekView-iframe",
},
"flag.coep": "true",
layout: "week_view",
},
}
@@ -487,6 +519,7 @@ if (only === "all" || only == "ns:columnView") {
iframeAttrs: {
id: "cal-booking-place-columnView-iframe",
},
"flag.coep": "true",
layout: "column_view",
},
}
@@ -165,3 +165,13 @@ export async function installAppleCalendar(page: Page) {
await page.waitForURL("/apps/apple-calendar");
await page.click('[data-testid="install-app-button"]');
}
export async function assertNoRequestIsBlocked(page: Page) {
page.on("requestfailed", (request) => {
const error = request.failure()?.errorText;
// Identifies that the request is blocked by the browser due to COEP restrictions
if (error?.includes("ERR_BLOCKED_BY_RESPONSE")) {
throw new Error(`Request Blocked: ${request.url()}. Error: ${error}`);
}
});
}
@@ -2,10 +2,16 @@ import { expect } from "@playwright/test";
import { test } from "@calcom/web/playwright/lib/fixtures";
import { bookFirstEvent, deleteAllBookingsByEmail, getEmbedIframe, todo } from "../lib/testUtils";
import {
assertNoRequestIsBlocked,
bookFirstEvent,
deleteAllBookingsByEmail,
getEmbedIframe,
todo,
} from "../lib/testUtils";
test.describe("Inline Iframe", () => {
test("Inline Iframe - Configured with Dark Theme", async ({
test("Inline Iframe - Configured with Dark Theme. Do booking and verify that COEP/CORP headers are correctly set", async ({
page,
embeds: { addEmbedListeners, getActionFiredDetails },
}) => {
@@ -24,10 +30,33 @@ test.describe("Inline Iframe", () => {
if (!embedIframe) {
throw new Error("Embed iframe not found");
}
assertNoRequestIsBlocked(page);
await bookFirstEvent("pro", embedIframe, page);
await deleteAllBookingsByEmail("embed-user@example.com");
});
test("COEP flag if not enabled, embed is blocked", async ({ page, embeds }) => {
const embedBlockedPromise = new Promise((resolve) => {
page.on("requestfailed", (request) => {
const error = request.failure()?.errorText;
// Identifies that the request is blocked by the browser due to COEP restrictions
if (error?.includes("ERR_BLOCKED_BY_RESPONSE")) {
console.log("Request failed: ", request.url(), error);
resolve(request.url().includes("/pro/embed"));
}
});
});
const calNamespace = "corpTest";
await embeds.gotoPlayground({ calNamespace, url: `?only=ns:${calNamespace}` });
await embedBlockedPromise.then((isBlocked) => {
expect(isBlocked).toBe(true);
});
});
todo(
"Ensure that on all pages - [user], [user]/[type], team/[slug], team/[slug]/book, UI styling works if these pages are directly linked in embed"
);
@@ -23,6 +23,7 @@ export type PrefillAndIframeAttrsConfig = Record<string, string | string[] | Rec
// TODO: It should have a dedicated prefill prop
// prefill: {},
"flag.coep"?: "true" | "false";
// TODO: Move layout and theme as nested props of ui as it makes it clear that these two can be configured using `ui` instruction as well any time.
// ui: {layout; theme}
@@ -18,6 +18,12 @@ module.exports = defineConfig((configEnv) => {
}),
...(process.argv.includes("--https") ? [basicSsl()] : []),
],
server: {
// Helps us to test that embed works with these headers
headers: {
"Cross-Origin-Embedder-Policy": "require-corp",
},
},
build: {
emptyOutDir: true,
rollupOptions: {