The Silent Login Killer: Debugging CSP in Next.js
It's a classic scenario: you pull the latest code, fire up npm run dev, and... nothing happens. You click the big "Sign In" button, and it stares back at you, unresponsive. No loading spinner, no redirect, just silence.
This is exactly what happened to us recently while refining the Extractify design system. Here’s a dive into the rabbit hole of Content Security Policies (CSP) and local development quirks.
The Symptoms
-
The Ghost Button: Clicking "Sign in with Google" triggered zero visible action.
-
The Console Error: Buried in the browser console was a scary-looking red message:
Uncaught EvalError: Evaluating a string as JavaScript violates the following Content Security Policy directive...
The Culprit: Too Much Security?
We recently hardened our security headers in next.config.mjs to prepare for production. One of these headers is the Content Security Policy (CSP), which tells the browser exactly what sources of code are allowed to run.
To prevent Cross-Site Scripting (XSS) attacks, we blocked unsafe-eval.
The Catch: Next.js (and Webpack) often uses eval() in development mode to generate source maps and handle hot module reloading (HMR). By blocking eval, we effectively broke the JavaScript that runs our local development environment, including the event listeners for our login button.
The Fix: Conditional Security
We can't just enable unsafe-eval everywhere—that would leave our production users vulnerable. The solution is to make our CSP smart enough to know when it's running locally versus in production.
Here is how we refactored our next.config.mjs:
const nextConfig = {
async headers() {
// Detect if we are in development mode
const isDev = process.env.NODE_ENV === 'development';
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
// Only allow unsafe-eval locally!
`script-src 'self' 'unsafe-inline' ${isDev ? "'unsafe-eval'" : ""} https://accounts.google.com`,
// Allow local API connections
`connect-src 'self' ${isDev ? "http://localhost:*" : ""} https://api.extractifyhq.com`,
// ... other rules
].join('; ')
}
]
}
];
}
};
Lesson Learned
Security is a balancing act. Strict policies are essential for protecting user data, but they need to be flexible enough to not hinder the developer experience. Always test your security headers in a production build locally before deploying, and use environment variables to toggle developer-specific rules.