Repository: next.js
Issue: Unable to add nonce to script - difference between Server and Client using Next.js 14 App Router and Middleware (Content Security Policy)
Labels: bug,TypeScript,Lazy Loading,Runtime

### Link to the code that reproduces this issue

https://codesandbox.io/p/devbox/nextjs-approuter-middleware-content-security-policy-cs37j3

### To Reproduce

I created a Codesandbox of the code and it is giving me a different error than what I see locally, but it's the same issue: 
- Codesandbox env: 'Hydration failed because the initial UI does not match what was rendered on the server.'
- Local on my Mac: app-index.js:35 Warning: Prop `nonce` did not match. Server: "" Client: "ODMzNThhNzItYzgwYy00ZTk3LWJmYjItMDZiMDlhNGE1Y2Vh"
- No I do not want to use `suppressHydrationWarning` in `layout.tsx` as that isn't a good security move.

🔗 Codsandbox link: https://codesandbox.io/p/devbox/nextjs-approuter-middleware-content-security-policy-cs37j3

1. Clone starter repo: https://github.com/vercel/next.js/tree/canary/examples/with-strict-csp
2. Install these packages (package.json attached):
```json
{
  "name": "next-testing-content-security-policy",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@emotion/cache": "^11.11.0",
    "@emotion/react": "^11.11.4",
    "@emotion/styled": "^11.11.0",
    "@mui/icons-material": "^5.15.14",
    "@mui/material": "^5.15.14",
    "@mui/material-nextjs": "^5.15.11",
    "next": "14.1.4",
    "react": "^18",
    "react-dom": "^18"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "14.1.4",
    "typescript": "^5"
  }
}

```
3. Create a theming script for light mode / dark mode (without white flash) - so it needs to pull from the requests in order to render out the right solution. This is my code in the steps to reproduce:


The functionality is a light mode and dark mode toggle and it works exactly the way that it should where it will check on the server what the preference is set for the user and then pass that into the app via next.js 14 app router middleware.

This also gets rid of the FOUC (i think that's what it's called) where there is a white flash on the dark option before rendering/recognizing that it was the selected version. This white half-second flash doesn't occur, which is ideal. That's what I mean when I say the feature is perfect except for this nonce did not match error.

Here is my code and let's debug together:


middleware.ts
```ts
import { NextRequest, NextResponse } from "next/server";

export function middleware(request: NextRequest) {
    const nonce = Buffer.from(crypto.randomUUID()).toString("base64") || '';
    console.log('nonce in middlweare', `${nonce}.........`)
    const cspHeader = `
        default-src 'self';
        script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https: ${
        process.env.NODE_ENV === "production" ? "" : `'unsafe-eval' http:`
    };
        style-src 'self' 'nonce-${nonce}';
        img-src 'self' blob: data: https://upload.wikimedia.org;
        font-src 'self';
        object-src 'none';
        base-uri 'self';
        form-action 'self';
        frame-ancestors 'none';
        block-all-mixed-content;
        upgrade-insecure-requests;
    `;
    {/* 
    
        default-src 'self';
        script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https: http: 'unsafe-inline' ${
        process.env.NODE_ENV === "production" ? "" : `'unsafe-eval'`
    };
        style-src 'self' 'nonce-${nonce}';
        img-src 'self' blob: data: https://upload.wikimedia.org;
        font-src 'self';
        object-src 'none';
        base-uri 'self';
        form-action 'self';
        frame-ancestors 'none';
        block-all-mixed-content;
        upgrade-insecure-requests;
    
    */}

    // Replace newline characters and spaces
    const contentSecurityPolicyHeaderValue = cspHeader
        .replace(/\s{2,}/g, " ")
        .trim();

    const requestHeaders = new Headers(request.headers);
    requestHeaders.set("x-nonce", nonce);
    requestHeaders.set(
        "Content-Security-Policy",
        contentSecurityPolicyHeaderValue,
    );

    const response = NextResponse.next({
        request: {
        headers: requestHeaders,
        },
    });

    // Add CSP
    response.headers.set('Content-Security-Policy', contentSecurityPolicyHeaderValue);

    // Tell browser to only access via HTTPS
    response.headers.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
    
    // Prevent browser from guessing tyupe of content if Content-Type header not set, prevent XSS exploits
    response.headers.set('X-Content-Type-Options', 'nosniff');
    
    // Control info sent when navigating to another 
    response.headers.set('Referrer-Policy', 'origin-when-cross-origin');

    // Set nonce cookie
    response.cookies.set('csp-nonce', nonce, { httpOnly: true, sameSite: 'strict' });
    
    return response;
}

export const config = {
    matcher: [
        /*
        * Match all request paths except for the ones starting with:
        * - api (API routes)
        * - _next/static (static files)
        * - _next/image (image optimization files)
        * - favicon.ico (favicon file)
        */
        {
        source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
        missing: [
            { type: "header", key: "next-router-prefetch" },
            { type: "header", key: "purpose", value: "prefetch" },
        ],
        },
    ],
};
```

app/page.tsx
```tsx
import { headers } from "next/headers";
import Script from "next/script";
import Link from "next/link";
import SVGItem from "./components/SVGItem";
import TestClient from "./components/TestClient";
import TestImageComponent from "./components/TestImageComponent";
import Image from "./components/lib/Image";
import Head from "next/head";
import { Metadata } from "next";

import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';
import { LinearProgress } from "@mui/material";

import dynamic from 'next/dynamic';
import LoadingThemeButton from '@/app/components/LoadingThemeButton';

const SetThemeButton = dynamic(() => import('@/app/components/SetThemeButton'), {
  ssr: false,
  loading: () => <LoadingThemeButton/>,
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app"
};

export default function Page() {
  const nonce = headers().get("x-nonce") ?? undefined;
  console.log('nonce in page', nonce)

  return (
    <main>
    <h1>hello</h1>
    {/* <LinearProgress color="success" /> */}
    <SetThemeButton />
    <h3>hello</h3>
    </main>
);
}
```

layout.tsx
```ts
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

// MUI
import { AppRouterCacheProvider } from '@mui/material-nextjs/v13-appRouter';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import {lightTheme, darkTheme} from '@/app/styles/theme';
import Script from "next/script";
import ThemeScript from "@/app/styles/ThemeToggle";
import { headers } from "next/headers";


// Font
const inter = Inter({ subsets: ["latin"] });

// export const metadata: Metadata = {
//   title: "Create Next App",
//   description: "Generated by create next app",
// };

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {

  // const nonce = headers().get("x-nonce");
  // console.log('nonce in layout', nonce)

  return (
    <html lang="en">
      <head>
        <ThemeScript />
      </head>
        <body className={inter.className}>
          <AppRouterCacheProvider>
            <ThemeProvider theme={darkTheme}>
              {children}
            </ThemeProvider>  
          </AppRouterCacheProvider>
        </body>
    </html>
  );
}
```


app/styles/ThemeToggle.tsx
```tsx
import { cookies, headers } from "next/headers";
import Script from "next/script";

type Theme = 'light' | 'dark';
type ThemeScriptProps = {
  nonce: string; // nonce is a required string
};

declare global {
  interface Window {
    __theme: Theme;
    __onThemeChange: (theme: Theme) => void;
    __setPreferredTheme: (theme: Theme) => void;
  }
}

function code() {
  window.__onThemeChange = function () {};

  function setTheme(newTheme: Theme) {
    window.__theme = newTheme;
    preferredTheme = newTheme;
    document.documentElement.dataset.theme = newTheme;
    window.__onThemeChange(newTheme);
  }

  var preferredTheme;

  try {
    preferredTheme = localStorage.getItem('theme') as Theme;
  } catch (err) {}

  window.__setPreferredTheme = function (newTheme: Theme) {
    setTheme(newTheme);
    try {
      localStorage.setItem('theme', newTheme);
    } catch (err) {}
  };

  var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');

  darkQuery.addEventListener('change', function (e) {
    window.__setPreferredTheme(e.matches ? 'dark' : 'light');
  });

  setTheme(preferredTheme || (darkQuery.matches ? 'dark' : 'light'));
}

export default function ThemeScript() {
  const nonce = headers().get("x-nonce") ?? undefined;
  console.log('nonce in theme', nonce)

  // const nonce = cookies().get("csp-nonce") ?? undefined;
  // console.log('nonce in theme', nonce?.value!)
  
  return (
    <script nonce={nonce} dangerouslySetInnerHTML={{ __html: `(${code})();` }} />

  // return (
    // <Script 
    //   src="public/theme-switcher.js"
    //   nonce={nonce}
    //   strategy="beforeInteractive"
    // />
  )

}
```


components/LoadingThemeButton.tsx
```tsx
const LoadingThemeButton = () => {
    return <button>loading...</button>;
  };
  
  export default LoadingThemeButton;
```

app/components/SetTheme.tsx
```tsx
'use client'
import React, { useState, useEffect } from 'react';

const SetTheme = (): JSX.Element => {
  const [theme, setTheme] = useState<string>((global.window as any)?.__theme || 'dark');

  const isDark = theme === 'dark';

  const toggleTheme = () => {
    (global.window as any)?.__setPreferredTheme(theme === 'light' ? 'dark' : 'light');
  };

  useEffect(() => {
    (global.window as any).__onThemeChange = setTheme;
  }, []);

  return <button onClick={toggleTheme}>{isDark ? 'Light' : 'Dark'}</button>;
};

export default SetTheme;

```

app/components/SetThemeButton.tsx
```tsx
'use client';
import { useState, useEffect } from 'react';

const SetThemeButton = ({ nonce }: {nonce: string}) => {
  const [theme, setTheme] = useState(global.window?.__theme || 'light');
  console.log('nonce from SetThemeButton', nonce)

  const isDark = theme === 'dark';

  const toggleTheme = () => {
    global.window?.__setPreferredTheme(isDark ? 'light' : 'dark');
  };

  useEffect(() => {
    global.window.__onThemeChange = setTheme;
  }, []);

  return <button id="ThemeToggleButton" nonce={nonce} onClick={toggleTheme}>{isDark ? 'dark' : 'light'}</button>;
};

export default SetThemeButton;
```

### Current vs. Expected behavior

Current Behavior:
- Right now, if I comment out <ThemeScript/> from the `layout.tsx`'s `<head>` object, then the error goes away
- This tells me that the nonce is not being passed correctly from my middleware.tsx file to layout.tsx into the <ThemeScript/> even though I should be able to get this via the method of `const nonce = headers().get("x-nonce") ?? undefined;` as I am doing in my ThemeScript() function.
- Yes, I have read the docs on Vercel about CSP and nonces ((linked here)[https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy]), but this does not address the issue where I need to add a nonce to a <script>
   - I saw @leerob's posts about this in the issues which helped me get a good jump start, but now I am stuck with the nonce issue that won't let me allowlist scripts via nonces for a CSP with next.

Expected Behavior:
- I would expect the server and client to have the same nonce based on how the script is run so that the <script> would be allowlisted

Here is a screenshot of what I see:
<img width="1420" alt="Screenshot 2024-03-27 at 12 14 33 AM" src="https://github.com/vercel/next.js/assets/56947075/8b5a23fc-4d9e-4764-93fe-d7c3eafcbed8">


### Provide environment information

```bash
Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 22.3.0: Mon Jan 30 20:38:37 PST 2023; root:xnu-8792.81.3~2/RELEASE_ARM64_T6000
Binaries:
  Node: 18.17.1
  npm: 9.6.7
  Yarn: N/A
  pnpm: N/A
Relevant Packages:
  next: 14.1.4
  eslint-config-next: 14.1.4
  react: 18.2.0
  react-dom: 18.2.0
  typescript: 5.4.3
Next.js Config:
  output: N/A
```


### Which area(s) are affected? (Select all that apply)

App Router, Dynamic imports (next/dynamic), Middleware / Edge (API routes, runtime), TypeScript (plugin, built-in types)

### Which stage(s) are affected? (Select all that apply)

next dev (local)

### Additional context

I am running this locally and also in a codesandbox, and the issues remain similar.