next-auth icon indicating copy to clipboard operation
next-auth copied to clipboard

fix(next-auth): Add support for Next.js basePath configuration

Open k3k8 opened this issue 9 months ago • 15 comments

☕️ Reasoning

When I add basePath configuration to Next.JS config, the API routes are broken. If both AUTH_URL and NextAuth basePath configurations are empty, NextAuth will look like it works correctly. But, OAuth redirects callback URI is omitted basePath, so will fail.

In case of settings with AUTH_URL with full path: http://localhost:3000/basepath/api/auth, or adding NextAuth basePath configuration like /basepath/api/auth, NextAuth will raise Unknown Action Error.

I’ve investigated this error and figured out that it is caused by the Next.JS specification or bug.
NextAuth parses the NextRequest URL and creates an action with the request URL. But, Next.JS with basePath configuration reports that the URL does not contain basePath. The simple Next.JS project here reproduces this issue. 
https://github.com/k3k8/nextjs-base-path-issue

This issue has been reported in the Next.JS repository, but has not yet received any responses from the members. (https://github.com/vercel/next.js/issues/60956) I suspect that the Next.JS app router should rewrite from an inbound URL to an internal one. Thus this is likely to be a specification, in my opinion.

Consequently, I’ve decided to fix this problem with next-auth components. The problem is caused by the Next.JS specification; my commits are only on the next-auth package. No commits are on core libraries.

I’ve created reqWithBasePathURL and estimateBasePath on packages/next-auth/src/lib/env.ts and modified reqWithEnvURL call point.

Configuration sample: Next.Js baesPaht: “/base_path”, NextAuth config.basePath: “/base_path/api/auth” The reqWithEnvURL called POST or GET request, and req: NextRequest contains like this: http://localhost:3000/api/auth/session 
The estimateBasePath function estimates from the request URL and config.BasePath.

If you set a Next.JS basePath such as /base_path, the NextAuth config should be contain full path to auth dir.
AUTH_URL: http://localhost:3000/base_path/api/auth, or basePath: /base_path/api/auth. Configuring the full path to AUTH_URL is a v4 specification, and this correct configuration also works.
AUTH_URL: http://locahost:3000/, basePath: /base_path/api/auth

These configuration rules are obeyed on document one (https://authjs.dev/reference/warnings#env-url-basepath-redundant).

🧢 Checklist

  • [x] Documentation
  • [x] Tests
  • [x] Ready to be merged

🎫 Affected issues

#9274 #9984 #10009

📌 Resources

k3k8 avatar May 02 '24 10:05 k3k8

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
auth-docs ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 1, 2024 10:24am
1 Skipped Deployment
Name Status Preview Comments Updated (UTC)
next-auth-docs ⬜️ Ignored (Inspect) Visit Preview Aug 1, 2024 10:24am

vercel[bot] avatar May 02 '24 10:05 vercel[bot]

@k3k8 is attempting to deploy a commit to the authjs Team on Vercel.

A member of the Team first needs to authorize it.

vercel[bot] avatar May 02 '24 10:05 vercel[bot]

Eagerly looking forward to see this PR into production, the basePath configuration is quite necessary.

tunchunairarko avatar May 24 '24 14:05 tunchunairarko

@ThangHuuVu, @balazsorban44 Is there any way you can check / merge this PR? auth-js is currently unusable for me because I need to have my application behind a basePath.

forbesd7 avatar Jul 04 '24 04:07 forbesd7

+1 on this, my app needs a basePath as well

PauloJSGG avatar Jul 16 '24 23:07 PauloJSGG

even my app needs a basePath as well

atularora82 avatar Jul 24 '24 18:07 atularora82

When using SessionProvider, I was able to specify basePath in layout.tsx like so and get it to work. Had to provide hostname without which it won't work. Not sure of the implications, any thoughts on this?:

.env.local

BASE_PATH=/basepath

layout.tsx

<SessionProvider basePath={`http://localhost:3000${process.env.BASE_PATH}/api/auth`}>

Code-DJ avatar Jul 24 '24 19:07 Code-DJ

I have tried configuring a base path with NextAuth including putting the basePath in authConfig, in SessionProvider with the relative or the full URL, as the AUTH_URL. Sadly, none of them work, waiting for this fix to get merged

itsskofficial avatar Jul 25 '24 07:07 itsskofficial

Spent the past day trying all sorts of workarounds to get basePath to work then found this PR, really strange that there is no update on this.

ejzeronimo avatar Jul 26 '24 16:07 ejzeronimo

Yeah, I just hope the next-auth team takes a look at this. A lot of apps need a basePath. Our team actually tried different implementations without success. We are seriously thinking of removing next-auth all together due to this 🫤. We like this library, but a basePath is mandatory for us.

PauloJSGG avatar Jul 26 '24 16:07 PauloJSGG

I understand the issue, thanks for putting together a thoughtful PR @k3k8 and everyone for flagging this. I actually ran into this issue a few months ago when I build the example to use next-auth deployed behind a proxy. At that time my workaround is basically appending the base path like this:

// app/api/auth/[...nextauth]/route.ts
import { handlers } from "auth";
import { NextRequest } from "next/server";

const reqWithBasePath = (req: NextRequest, basePath = "/authjs") => {
  const baseUrl = new URL(
    (req.headers.get("x-forwarded-proto") ?? "https") +
      "://" +
      (req.headers.get("x-forwarded-host") ?? req.nextUrl.host)
  );
  const urlWithBasePath = new URL(
    `${basePath}${req.nextUrl.pathname}${req.nextUrl.search}`,
    baseUrl
  );
  return new NextRequest(urlWithBasePath.toString(), req);
};

export const GET = (req: NextRequest) => {
  const nextReq = reqWithBasePath(req);
  return handlers.GET(nextReq);
};

export const POST = (req: NextRequest) => {
  const nextReq = reqWithBasePath(req);
  return handlers.POST(nextReq);
};

https://github.com/ThangHuuVu/next-auth-behind-proxy/blob/cde4368b46fbda57157d62eaf1b93b258f5fadde/app/api/auth/%5B...nextauth%5D/route.ts

I'm a bit hesitant to merge this PR, though. I would actually prefer an official next.js API to get the basePath value (this isn't exist yet AFAIK) instead of calling the estimateBasePath function. The reason is that functions like estimateBasePath is quite complex and could easily introduce bugs, we need at least some tests to make sure it won't break any existing behaviors.

ThangHuuVu avatar Jul 27 '24 04:07 ThangHuuVu

I understand the complexity of the estimateBasePath function. I created it while testing with the following test cases. These tests were created based on packages/core/test/url-parsing.test.ts. Adding these test cases would ensure that future changes don't break existing behavior.

function estimateBasePath(configPathName: string, requestPathname: string): string | undefined {
  const configSegments = configPathName.slice(1).split("/")
  const requestSegments = requestPathname.slice(1).split("/")
  if (configSegments[0] === requestSegments[0]) return undefined

  let i = 0
  while (i < configSegments.length && i < requestSegments.length && configSegments[i] !== requestSegments[0]) {
    i++
  }
  return "/" + configSegments.slice(0, i).join("/")
}

const testCases = [
  { configPathName: '/base_path/api/auth', requestPathname: '/api/auth/session', expected: '/base_path' },
  { configPathName: '/api/auth', requestPathname: '/api/auth/session', expected: '' },
  { configPathName: '/test_path/api/auth', requestPathname: '/api/auth/signin/google', expected: '/test_path' },
  { configPathName: '/api/auth', requestPathname: '/api/auth/signin/google', expected: '' },
  { configPathName: '/test_path/test2/api/auth', requestPathname: '/api/auth/signout', expected: '/test_path/test2' },
  { configPathName: '/auth', requestPathname: '/auth/signin', expected: '' },
  { configPathName: '/sso/auth', requestPathname: '/auth/signin', expected: '/sso' },
];

testCases.forEach(({ configPathName, requestPathname, expected }) => {
  const result = estimateBasePath(configPathName, requestPathname);
  console.log(`Config: ${configPathName}, Request: ${requestPathname}, Expected: ${expected}, Result: ${result}`);
  console.assert(result === expected, 'Test case failed');
});

The solution of adding the base path in route.ts, as @ThangHuuVu's suggested, works well in my environment too and seems like a good approach.

Given the numerous comments on this issue, I'd like to implement one of the following:

  1. Add information about the base path issue and @ThangHuuVu's code to the documentation
  2. Add these test cases to the next-auth package to test estimateBasePath

I'm planning to commit to either of these options.

I'd appreciate your thoughts on these proposals. I want to contribute in the way that best suits the project.

k3k8 avatar Aug 01 '24 08:08 k3k8

Any update on getting this fix in? I'm hitting it as well... setting a nextjs base path breaks the oauth redirect_uri, or if you set the nextjs base path and set a authjs base path to match, then you get Unknown Action (because the pathname doesn't match the base path). I can't find any way around this...

jbogedahl-st avatar Aug 02 '24 20:08 jbogedahl-st

Any update on getting this fix in? I'm hitting it as well... setting a nextjs base path breaks the oauth redirect_uri, or if you set the nextjs base path and set a authjs base path to match, then you get Unknown Action (because the pathname doesn't match the base path). I can't find any way around this...

I found one way around this by remapping my routes in my route.ts file. That fixes things on the server, however, now I'm hitting issues on the client. I can't seem to get the client to call the apis with the correct path. I consistently get 404 errors when I have a base path (ie. not the default localhost:300/api/auth, but I have localhost:4000/app/api/auth). It never calls the correct api urls, just the default.

jbogedahl-st avatar Aug 08 '24 19:08 jbogedahl-st

Can you please try the solution from ThangHuuVu's most recent reply? If you want to resolve the issue sooner, this would be the most effective approach.

Alternatively, you can test my code using the following method:

git clone -b fix-base-path https://github.com/k3k8/next-auth.git
cd next-auth
corepack enable pnpm
pnpm install
pnpm build
cd packages/next-auth/
pnpm pack

This will generate a package file named something like next-auth-5.0.0-beta.20.tgz.

Move this file to your project directory and install it by updating your package.json:

{
  "dependencies": {
    "next-auth": "./next-auth-5.0.0-beta.20.tgz"
  }
}

Then run pnpm install to update your dependencies.

k3k8 avatar Aug 22 '24 05:08 k3k8