nextauthjs / next-auth

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

unable to to use subdomain in AUTH_URL and wrap middleware in auth - next-auth v5 #9631

Open trevorpfiz opened 8 months ago

trevorpfiz commented 8 months ago

Environment

System: OS: Linux 5.15 Ubuntu 20.04.6 LTS (Focal Fossa) CPU: (16) x64 AMD Ryzen 7 3700X 8-Core Processor Memory: 4.78 GB / 7.76 GB Container: Yes Shell: 5.8 - /usr/bin/zsh Binaries: Node: 20.10.0 - ~/.nvm/versions/node/v20.10.0/bin/node npm: 10.2.3 - ~/.nvm/versions/node/v20.10.0/bin/npm pnpm: 8.14.1 - ~/.local/share/pnpm/pnpm bun: 1.0.14 - ~/.local/share/pnpm/bun Watchman: 2023.12.04.00 - /home/linuxbrew/.linuxbrew/bin/watchman

Reproduction URL

https://github.com/trevorpfiz/nourish.run/tree/01-14-middleware_works_without_auth

Describe the issue

Error: getaddrinfo ENOTFOUND app.localhost
    at GetAddrInfoReqWrap.onlookupall [as oncomplete] (node:dns:118:26)
    at GetAddrInfoReqWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
  errno: -3008,
  code: 'ENOTFOUND',
  syscall: 'getaddrinfo',
  hostname: 'app.localhost'
}

How to reproduce

Use a subdomain for localhost. Wrap middleware in auth. Don't include app.localhost in /etc/hosts file.

AUTH_URL='http://app.localhost:3000'

Expected behavior

In v4 I am able to use NEXTAUTH_URL='http://app.localhost:3000' without the error in the platforms starter.

trevorpfiz commented 8 months ago

I am setting a cookie on localhost and want it to be accessible in the subdomains like app.localhost. It does not seem like that can be done, so I am following this approach: Session-token cookie not set #4089

if I set AUTH_URL='http://local.run:3000' and go to localhost:3000, the req.url is local.run:3000. I also get an infinite loop when doing

return NextResponse.rewrite(
      new URL(`/home${path === "/" ? "" : path}`, req.url),
    );

This is not consistent with the behavior of not wrapping the middleware in auth, where the req.url is localhost:3000 and I do not get an infinite loop.

This works "correctly":

import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";

import { env } from "~/env";

export const config = {
  matcher: [
    /*
     * Match all paths except for:
     * 1. /api/ routes
     * 2. /_next/ (Next.js internals)
     * 3. /_proxy/ (special page for OG tags proxying)
     * 4. /_static (inside /public)
     * 5. /_vercel (Vercel internals)
     * 6. Static files (e.g. /favicon.ico, /sitemap.xml, /robots.txt, etc.)
     */
    "/((?!api/|_next/|_proxy/|_static|_vercel|[\\w-]+\\.\\w+).*)",
  ],
};

export default async function middleware(req: NextRequest) {
  const url = req.nextUrl;

  // Get hostname of request (e.g. demo.vercel.pub, demo.localhost:3000)
  let hostname = req.headers
    .get("host")!
    .replace(".localhost:3000", `.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`)
    .replace(".local.run:3000", `.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`);

  // special case for Vercel preview deployment URLs
  if (
    hostname.includes("---") &&
    hostname.endsWith(`.${process.env.NEXT_PUBLIC_VERCEL_DEPLOYMENT_SUFFIX}`)
  ) {
    hostname = `${hostname.split("---")[0]}.${env.NEXT_PUBLIC_ROOT_DOMAIN}`;
  }

  const searchParams = req.nextUrl.searchParams.toString();
  // Get the pathname of the request (e.g. /, /about, /blog/first-post)
  const path = `${url.pathname}${
    searchParams.length > 0 ? `?${searchParams}` : ""
  }`;

  console.log(hostname, path, url, req.url);

  // Handle app subdomain
  if (hostname == `app.${env.NEXT_PUBLIC_ROOT_DOMAIN}`) {
    console.log(hostname, req.url, "apppppppppp");
    // if (!isLoggedIn) {
    //   return NextResponse.redirect(new URL("/signin", req.url));
    // }
    return NextResponse.rewrite(
      new URL(`/app${path === "/" ? "" : path}`, req.url),
    );
  }

  // Handle main domain
  if (
    hostname === "localhost:3000" ||
    hostname === "local.run:3000" ||
    hostname.endsWith(`.${env.NEXT_PUBLIC_ROOT_DOMAIN}`) // e.g. www.domain.com
  ) {
    console.log(hostname, req.url, "mainnnnnnn");

    const isDevelopment = process.env.NODE_ENV === "development";
    const baseUrl = isDevelopment
      ? `http://app.${hostname}`
      : `https://app.${env.NEXT_PUBLIC_ROOT_DOMAIN}`;

    return NextResponse.rewrite(
      new URL(`/home${path === "/" ? "" : path}`, req.url),
    );
  }

  // Allow normal processing for all other requests
  return NextResponse.next();
}

This gives whatever value is set in AUTH_URL for the req.url and will give an infinite loop on the /home rewrite:

import { NextResponse } from "next/server";

import { auth } from "@nourish/auth";

import { env } from "~/env";
import { authRoutes } from "~/routes";

export const config = {
  matcher: [
    /*
     * Match all paths except for:
     * 1. /api/ routes
     * 2. /_next/ (Next.js internals)
     * 3. /_proxy/ (special page for OG tags proxying)
     * 4. /_static (inside /public)
     * 5. /_vercel (Vercel internals)
     * 6. Static files (e.g. /favicon.ico, /sitemap.xml, /robots.txt, etc.)
     */
    "/((?!api/|_next/|_proxy/|_static|_vercel|[\\w-]+\\.\\w+).*)",
  ],
};

export default auth((req) => {
  const url = req.nextUrl;
  const isLoggedIn = !!req.auth;

  // Get hostname of request (e.g. demo.vercel.pub, demo.localhost:3000)
  let hostname = req.headers
    .get("host")!
    .replace(".localhost:3000", `.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`)
    .replace(".local.run:3000", `.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`);

  // special case for Vercel preview deployment URLs
  if (
    hostname.includes("---") &&
    hostname.endsWith(`.${process.env.NEXT_PUBLIC_VERCEL_DEPLOYMENT_SUFFIX}`)
  ) {
    hostname = `${hostname.split("---")[0]}.${env.NEXT_PUBLIC_ROOT_DOMAIN}`;
  }

  const searchParams = req.nextUrl.searchParams.toString();
  // Get the pathname of the request (e.g. /, /about, /blog/first-post)
  const path = `${url.pathname}${
    searchParams.length > 0 ? `?${searchParams}` : ""
  }`;

  console.log(hostname, path, url, req.url);

  // Handle app subdomain
  if (hostname == `app.${env.NEXT_PUBLIC_ROOT_DOMAIN}`) {
    console.log(isLoggedIn, hostname, req.url, "apppppppppp");
    // if (!isLoggedIn) {
    //   return NextResponse.redirect(new URL("/signin", req.url));
    // }
    return NextResponse.rewrite(
      new URL(`/app${path === "/" ? "" : path}`, req.url),
    );
  }

  // Handle main domain
  if (
    hostname === "localhost:3000" ||
    hostname === "local.run:3000" ||
    hostname.endsWith(`.${env.NEXT_PUBLIC_ROOT_DOMAIN}`) // e.g. www.domain.com
  ) {
    console.log(isLoggedIn, hostname, req.url, "mainnnnnnn");

    const isDevelopment = process.env.NODE_ENV === "development";
    const baseUrl = isDevelopment
      ? `http://app.${hostname}`
      : `https://app.${env.NEXT_PUBLIC_ROOT_DOMAIN}`;

    if (isLoggedIn && authRoutes.includes(path)) {
      return NextResponse.redirect(new URL(baseUrl));
    }

    return NextResponse.rewrite(
      new URL(`/home${path === "/" ? "" : path}`, req.url),
    );
  }

  // Allow normal processing for all other requests
  return NextResponse.next();
});
trevorpfiz commented 8 months ago

simply put

  1. why is req.url different in middleware vs auth wrapper? req.url is always the same as AUTH_URL.
  2. if AUTH_URL is set to something other than localhost:3000, I am getting hosts errors and weird behaviors like infinite loops on rewrites.
trevorpfiz commented 8 months ago

my current workaround is to not wrap the middleware with auth, I just use it like this:

export default async function middleware(req: NextRequest) {
  const url = req.nextUrl;
  const session = await auth();
  const isLoggedIn = !!session?.user;