nextauthjs / next-auth

Authentication for the Web.
https://authjs.dev
ISC License
24.1k stars 3.34k forks source link

How can I set a request header within a Next.js middleware that is wrapped with withAuth? #8023

Closed rhmnaulia closed 7 months ago

rhmnaulia commented 1 year ago

Question 💬

I'm facing an issue while trying to set the Content-Security-Policy header within my Next.js middleware. It works perfectly when I use the middleware without withAuth, and the headers are successfully set. However, when I wrap it with withAuth, the header doesn't get set. I've checked the documentation, but couldn't find much information about it.

How to reproduce ☕️

  1. Start with the code provided in the following link: https://github.com/vercel/next.js/issues/42330#issuecomment-1542740389.
  2. Use the code within the withAuth wrapper.

    export default withAuth(
    function middleware(request: NextRequest) {
    const { csp, nonce } = generateCsp()
    const requestHeaders = new Headers(request.headers)
    
    requestHeaders.set("x-nonce", nonce)
    requestHeaders.set("content-security-policy", csp)
    
    const response = NextResponse.next({
      request: {
        headers: requestHeaders,
      },
    })
    
    console.log("Middleware executed!")
    
    response.headers.set("content-security-policy", csp)
    return response
    },
    {
    callbacks: {
      authorized: ({ token }) => {
        return !!token?.accessToken
      },
    },
    cookies: customCookies,
    pages: {
      signIn: "/",
      signOut: "/",
      error: "/",
    },
    }
    )
  3. Execute the code and observe that the console logs "Middleware executed!", but the header fails to be set.

Contributing 🙌🏽

No, I am afraid I cannot help regarding this

rexfordessilfie commented 1 year ago

Hi @rhmnaulia, could you please share an example of what you are doing on CodeSandbox/in a repo? I am wondering if you are using the Matcher in your middleware file? If you are, it would opt the middleware completely out of running on certain routes and so your CSP logic would never run. If this is not the case for you, then there could be something else at play! Thanks.

rhmnaulia commented 1 year ago

Hello @rexfordessilfie, I'm not using Matcher. If you see, I have added console.log there and the console is printed. However, the CSP logic won't run and the header won't set.

rexfordessilfie commented 1 year ago

Thanks for your response! I took a deeper look at your issue, and I was able to reproduce and resolve it locally.

Problem

Here's what I think is going on:

Solution

If you can afford to have your authentication pages (pages.signIn, pages.signOut etc) without CSP, then your current setup may work just fine.

If you do want CSP headers (or any other kind of headers) on every single page (while still enforcing authentication) you would have to apply withAuth differently. Specifically, you can do something such as this:

export async function middleware(request: NextRequestWithAuth) {
  console.log("Middleware executing!");

  // Prepare the CSP headers
  const { csp, nonce } = generateCsp();

  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("x-nonce", nonce);
  requestHeaders.set("content-security-policy", csp);

  // Execute the NextAuth middleware which either returns a redirect response or nothing, if authentication
  // was not required. See source for more: https://github.com/nextauthjs/next-auth/blob/v4/packages/next-auth/src/next/middleware.ts#L99
  // If a redirect was returned, use it. Otherwise continue the response normally with NextResponse.next().
  // Omitting the config here, but you can still include it (i.e withAuth(request, { pages: ... }))
  const response = (await withAuth(request)) || NextResponse.next();

  // Set the CSP headers on the response
  requestHeaders.forEach((value, key) => {
    response.headers.append(key, value);
  });

  console.log("Middleware executed!");

  return response;
}

Additional Notes

  1. Applying withAuth on all routes (as you have in your issue) as well as in the solution above, means that even static assets (e.g a logo) cannot be accessed without an authenticated user. You could solve this by introducing some kind of matching logic to exclude such static assets, or any others which you would not want to be blocked by authentication.
  2. Unrelated to the core issue, but the result of setting CSP headers on ALL routes resulted in some of the following errors:

    "Refused to send form data to 'http://localhost:3000/api/auth/signin/github' because it violates the following Content Security Policy directive: "form-action 'self'". " Refused to load the image 'https://authjs.dev/img/providers/github.svg' because it violates the following Content Security Policy directive: "img-src 'self' data:".

The first one essentially breaks next-auth's in-built authentication but hopefully you have an alternate CSP-safe approach for getting static assets and handling sign-in/out's!

stale[bot] commented 11 months ago

It looks like this issue did not receive any activity for 60 days. It will be closed in 7 days if no further activity occurs. If you think your issue is still relevant, commenting will keep it open. Thanks!

hoop71 commented 10 months ago

@rexfordessilfie rexfordessilfie

This is awesome! Thanks so much for your clear example and awesome project!