Skip to content

Commit f905ae7

Browse files
fix: Add CSP back to login page (calcom#22688)
* Middleware only logic to add CSP header as Next.js App Router doesnt allow setting header in server component * add tests * Remove nonce from Page Router and fix ts issues * Add checkly test
1 parent a1c0daa commit f905ae7

19 files changed

Lines changed: 567 additions & 220 deletions

__checks__/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# Checkly Tests
22

33
Run as `yarn checkly test`
4+
Optionally, run as `yarn checkly test --record` to be able to get detailed debugging view in Checkly UI
5+
Run the tests for a particylar file as `yarn checkly test {filePattern}`
46
Deploy the tests as `yarn checkly deploy`

__checks__/csp-login.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
test.describe("CSP Headers", () => {
4+
test("Login page should have CSP header with nonce", async ({ page }) => {
5+
const targetUrl = process.env.ENVIRONMENT_URL || "https://app.cal.com";
6+
7+
const response = await page.goto(`${targetUrl}/auth/login`);
8+
9+
expect(response?.status()).toBe(200);
10+
11+
const cspHeader = response?.headers()["content-security-policy"];
12+
expect(cspHeader).toBeTruthy();
13+
14+
// Verify nonce is present in CSP header
15+
const nonceMatch = cspHeader?.match(/'nonce-([^']+)'/);
16+
expect(nonceMatch).toBeTruthy();
17+
expect(nonceMatch![1]).toHaveLength(24);
18+
expect(nonceMatch![1]).toMatch(/==$/);
19+
20+
await page.screenshot({ path: "screenshot.jpg" });
21+
});
22+
});

apps/web/app/(booking-page-wrapper)/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import PageWrapper from "@components/PageWrapperAppDir";
44

55
export default async function BookingPageWrapperLayout({ children }: { children: React.ReactNode }) {
66
const h = await headers();
7-
const nonce = h.get("x-nonce") ?? undefined;
7+
const nonce = h.get("x-csp-nonce") ?? undefined;
88

99
return (
1010
<>

apps/web/app/(use-page-wrapper)/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import PageWrapper from "@components/PageWrapperAppDir";
55

66
export default async function PageWrapperLayout({ children }: { children: React.ReactNode }) {
77
const h = await headers();
8-
const nonce = h.get("x-nonce") ?? undefined;
8+
const nonce = h.get("x-csp-nonce") ?? undefined;
99
const headScript = process.env.NEXT_PUBLIC_HEAD_SCRIPTS;
1010
const bodyScript = process.env.NEXT_PUBLIC_BODY_SCRIPTS;
1111

apps/web/app/layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ const getInitialProps = async () => {
9696

9797
export default async function RootLayout({ children }: { children: React.ReactNode }) {
9898
const h = await headers();
99-
const nonce = h.get("x-csp") ?? "";
99+
const nonce = h.get("x-csp-nonce") ?? "";
100100

101101
const { locale, direction, isEmbed, embedColorScheme } = await getInitialProps();
102102

@@ -151,7 +151,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
151151
]}
152152
/>
153153

154-
<Providers isEmbed={isEmbed}>
154+
<Providers isEmbed={isEmbed} nonce={nonce}>
155155
<AppRouterI18nProvider translations={translations} locale={locale} ns={ns}>
156156
{children}
157157
</AppRouterI18nProvider>

apps/web/app/not-found.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const generateMetadata = async () => {
2121

2222
const ServerPage = async () => {
2323
const h = await headers();
24-
const nonce = h.get("x-nonce") ?? undefined;
24+
const nonce = h.get("x-csp-nonce") ?? undefined;
2525
const host = h.get("x-forwarded-host") ?? "";
2626

2727
return (

apps/web/app/providers.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ import PlainChat from "@lib/plain/dynamicProvider";
1313
type ProvidersProps = {
1414
isEmbed: boolean;
1515
children: React.ReactNode;
16+
nonce: string | undefined;
1617
};
17-
export function Providers({ isEmbed, children }: ProvidersProps) {
18+
export function Providers({ isEmbed, children, nonce }: ProvidersProps) {
1819
const isBookingPage = useIsBookingPage();
1920

2021
return (
2122
<SessionProvider>
2223
<TrpcProvider>
23-
{!isBookingPage ? <PlainChat /> : null}
24+
{!isBookingPage ? <PlainChat nonce={nonce} /> : null}
2425
{!isEmbed && !isBookingPage && <NotificationSoundHandler />}
2526
{/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */}
2627
<CacheProvider>

apps/web/components/PageWrapper.tsx

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,8 @@ function PageWrapper(props: AppProps) {
4444
pageStatus = "403";
4545
}
4646

47-
// On client side don't let nonce creep into DOM
48-
// It also avoids hydration warning that says that Client has the nonce value but server has "" because browser removes nonce attributes before DOM is built
49-
// See https://github.com/kentcdodds/nonce-hydration-issues
50-
// Set "" only if server had it set otherwise keep it undefined because server has to match with client to avoid hydration error
51-
const nonce = typeof window !== "undefined" ? (pageProps.nonce ? "" : undefined) : pageProps.nonce;
5247
const providerProps = {
5348
...props,
54-
pageProps: {
55-
...props.pageProps,
56-
nonce,
57-
},
5849
};
5950
// Use the layout defined at the page level, if available
6051
const getLayout = Component.getLayout ?? ((page) => page);
@@ -79,7 +70,6 @@ function PageWrapper(props: AppProps) {
7970
{...seoConfig.defaultNextSeo}
8071
/>
8172
<Script
82-
nonce={nonce}
8373
id="page-status"
8474
// It is strictly not necessary to disable, but in a future update of react/no-danger this will error.
8575
// And we don't want it to error here anyways

apps/web/lib/app-providers-app-dir.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import { useFlags } from "@calcom/features/flags/hooks";
1616
import useIsBookingPage from "@lib/hooks/useIsBookingPage";
1717
import useIsThemeSupported from "@lib/hooks/useIsThemeSupported";
1818
import type { WithLocaleProps } from "@lib/withLocale";
19-
import type { WithNonceProps } from "@lib/withNonce";
2019

2120
import type { PageWrapperProps } from "@components/PageWrapperAppDir";
2221

@@ -25,12 +24,11 @@ import { getThemeProviderProps } from "./getThemeProviderProps";
2524
// Workaround for https://github.com/vercel/next.js/issues/8592
2625
export type AppProps = Omit<
2726
NextAppProps<
28-
WithLocaleProps<
29-
WithNonceProps<{
30-
themeBasis?: string;
31-
session: Session;
32-
}>
33-
>
27+
WithLocaleProps<{
28+
nonce: string | undefined;
29+
themeBasis?: string;
30+
session: Session;
31+
}>
3432
>,
3533
"Component"
3634
> & {

apps/web/lib/app-providers.tsx

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import { useFlags } from "@calcom/features/flags/hooks";
2020

2121
import useIsBookingPage from "@lib/hooks/useIsBookingPage";
2222
import type { WithLocaleProps } from "@lib/withLocale";
23-
import type { WithNonceProps } from "@lib/withNonce";
2423

2524
import { useViewerI18n } from "@components/I18nLanguageHandler";
2625

@@ -33,13 +32,11 @@ const I18nextAdapter = appWithTranslation<
3332
// Workaround for https://github.com/vercel/next.js/issues/8592
3433
export type AppProps = Omit<
3534
NextAppProps<
36-
WithLocaleProps<
37-
WithNonceProps<{
38-
themeBasis?: string;
39-
session: Session;
40-
i18n?: SSRConfig;
41-
}>
42-
>
35+
WithLocaleProps<{
36+
themeBasis?: string;
37+
session: Session;
38+
i18n?: SSRConfig;
39+
}>
4340
>,
4441
"Component"
4542
> & {
@@ -72,12 +69,7 @@ const getEmbedNamespace = (query: ParsedUrlQuery) => {
7269
return typeof window !== "undefined" ? window.getEmbedNamespace() : (query.embed as string) || null;
7370
};
7471

75-
// We dont need to pass nonce to the i18n provider - this was causing x2-x3 re-renders on a hard refresh
76-
type AppPropsWithoutNonce = Omit<AppPropsWithChildren, "pageProps"> & {
77-
pageProps: Omit<AppPropsWithChildren["pageProps"], "nonce">;
78-
};
79-
80-
const CustomI18nextProvider = (props: AppPropsWithoutNonce) => {
72+
const CustomI18nextProvider = (props: AppPropsWithChildren) => {
8173
/**
8274
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
8375
**/
@@ -141,7 +133,7 @@ const enum ThemeSupport {
141133

142134
type CalcomThemeProps = PropsWithChildren<
143135
Pick<AppProps, "router"> &
144-
Pick<AppProps["pageProps"], "nonce" | "themeBasis"> &
136+
Pick<AppProps["pageProps"], "themeBasis"> &
145137
Pick<AppProps["Component"], "isBookingPage" | "isThemeSupported">
146138
>;
147139
const CalcomThemeProvider = (props: CalcomThemeProps) => {
@@ -253,7 +245,6 @@ function getThemeProviderProps({
253245
storageKey,
254246
forcedTheme,
255247
themeSupport,
256-
nonce: props.nonce,
257248
enableColorScheme: false,
258249
enableSystem: themeSupport !== ThemeSupport.None,
259250
// next-themes doesn't listen to changes on storageKey. So we need to force a re-render when storageKey changes
@@ -280,24 +271,13 @@ function OrgBrandProvider({ children }: { children: React.ReactNode }) {
280271

281272
const AppProviders = (props: AppPropsWithChildren) => {
282273
const isBookingPage = useIsBookingPage();
283-
const { pageProps, ...rest } = props;
284-
285-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
286-
const { nonce, ...restPageProps } = pageProps;
287-
const propsWithoutNonce = {
288-
pageProps: {
289-
...restPageProps,
290-
},
291-
...rest,
292-
};
293274

294275
const RemainingProviders = (
295276
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
296-
<CustomI18nextProvider {...propsWithoutNonce}>
277+
<CustomI18nextProvider {...props}>
297278
<TooltipProvider>
298279
<CalcomThemeProvider
299280
themeBasis={props.pageProps.themeBasis}
300-
nonce={props.pageProps.nonce}
301281
isThemeSupported={props.Component.isThemeSupported}
302282
isBookingPage={props.Component.isBookingPage || isBookingPage}
303283
router={props.router}>

0 commit comments

Comments
 (0)