next-auth
next-auth copied to clipboard
basePath doesn't work in Next.js - wrong redirect_uri or response with 400 BAD REQUEST
Environment
System: OS: Windows 10 10.0.19045 CPU: (28) x64 13th Gen Intel(R) Core(TM) i7-13850HX Memory: 16.98 GB / 31.61 GB Binaries: Node: 24.0.2 - C:\Program Files\nodejs\node.EXE npm: 11.4.0 - C:\Program Files\nodejs\npm.CMD Browsers: Edge: Chromium (134.0.3124.85) Internet Explorer: 11.0.19041.5794 npmPackages: next: 15.3.3 => 15.3.3 next-auth: ^5.0.0-beta.28 => 5.0.0-beta.28 react: ^19.0.0 => 19.1.0
Reproduction URL
https://github.com/vercel/next.js/tree/canary/examples/auth
Describe the issue
Using a basePath in Next.js makes next-auth either to send the invalid redirect_uri without the basePath, or just not work at all and return 400 BAD REQUEST for all endpoints.
Case 1: Having a basePath in Next.js but not providing it to the auth config.
// next.config.ts
import type { NextConfig } from "next"
const nextConfig: NextConfig = {
basePath: "/my-base-path",
}
export default nextConfig
// src/auth.ts
import NextAuth from "next-auth"
import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id"
export const { handlers, signIn, signOut, auth } = NextAuth({
// basePath: "/my-base-path/api/auth",
providers: [
MicrosoftEntraID({
clientId: process.env.AUTH_MICROSOFT_ENTRA_ID_ID,
clientSecret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET,
issuer: process.env.AUTH_MICROSOFT_ENTRA_ID_ISSUER,
}),
],
})
// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from '~/auth'
export const { GET, POST } = handlers
After the successful sign-in, it redirects to the /api/auth/callback/microsoft-entra-id (without the basePath). It is expected to be redirected to the /my-base-path/api/auth/callback/microsoft-entra-id path instead. Side note: the /my-base-path/api/auth/providers endpoint works properly.
Case 2: The basePath is provided to the auth config as well.
// auth.ts
import NextAuth from "next-auth"
import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id"
export const { handlers, signIn, signOut, auth } = NextAuth({
basePath: "/my-base-path/api/auth",
providers: [
MicrosoftEntraID({
clientId: process.env.AUTH_MICROSOFT_ENTRA_ID_ID,
clientSecret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET,
issuer: process.env.AUTH_MICROSOFT_ENTRA_ID_ISSUER,
}),
],
})
After the successful sign-in, it redirects to the correct /my-base-path/api/auth/callback/microsoft-entra-id path, but with a 400 BAD REQUEST. Side note: The /my-base-path/api/auth/providers responds with 400 as well.
How to reproduce
Just simply set up a clean Next.js application with Auth.js Microsoft Entra ID provider and use a basePath. All necessary code is seen above. Probably can be reproduced using a different provider, not sure.
Expected behavior
The expected behaviour is for it to actually work, or the documentation to be updated. Other info is the issue description.
@thepeterkovacs we have been experiencing the very same problem and have found a potential solution. Though it does involve a modification to the library.
Considering a PR.
Base path is not respected by all of Auth.JS's components especially the auth providers themselves.
In our case we are using an Oauth based provider and had tried to follow the documentation's suggested use of redirectproxyurl
However this was not respected.
We discovered that in the core file for authorisation URLs here performs a check to verify the usage of this URL.
However the evaluation will always fail. The first variable is set true in init if a redirectproxyurl URL is set. So the test in effect tests if set false and string is set true.
Our fix has been to change this check in two locations and adjust line 58 to ensure the data payload for verification is the same URL we set as the return URL
Found a simple temporary solution:
First, just add the desired redirectProxyUrl to the provider:
// auth.ts
import NextAuth from "next-auth"
import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id"
export const { handlers, signIn, signOut, auth } = NextAuth({
basePath: "/my-base-path/api/auth",
providers: [
MicrosoftEntraID({
clientId: process.env.AUTH_MICROSOFT_ENTRA_ID_ID,
clientSecret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET,
issuer: process.env.AUTH_MICROSOFT_ENTRA_ID_ISSUER,
// path to the auth endpoint, like http://localhost:3000/my-base-path/api/auth
redirectProxyUrl: `${process.env.NEXT_PUBLIC_URL}/my-base-path/api/auth`
}),
],
})
Then find the init.js file and change this one line:
// node_modules/@auth/core/lib/init.js
...
if ((provider?.type === "oauth" || provider?.type === "oidc") &&
provider.redirectProxyUrl) {
try {
// change the original '===' to a '!==' like this, or just delete this block entirely
isOnRedirectProxy = new URL(provider.redirectProxyUrl).origin !== url.origin
}
catch {
throw new TypeError(`redirectProxyUrl must be a valid URL. Received: ${provider.redirectProxyUrl}`)
}
}
...
Might have side effects in other providers, can't confirm.
From my work yesterday, related to https://github.com/issues/recent?issue=vercel%7Cnext.js%7C62756. Not sure if it is a bug or a feature not to include basepath. You also notice lots of places in the codebase of next auth beta where fallbacks overwrites are taking place with paths. differences depending on server side or client side code. depricated env variables and so on.
Made a fix pull request based on the advise in https://github.com/vercel/next.js/discussions/16059 where "NEXT_PUBLIC_BASE_PATH" in env variables is used to expose the basepath to both client side and server side code.
have not found a way to access the config basepath in all conditions. Something you would expect to exist. Lets hope this pull request can get merged not having to keep up to date a fork.
I experienced a similar issue, or at least I think so. Interestingly, #13174 did not fix my issue.
- I have
basePath: "/my-base-path/api/auth"configured inauth.ts - The signin route works fine, but the callback goes to
/my-base-path/api/auth/callback/keycloak - Next.js strips the base path but next-auth tries
pathname.match(new RegExp(`^${base}(.+)`))inweb.ts:125, which fails:
[auth][error] UnknownAction: Cannot parse action at /api/auth/callback/keycloak. Read more at https://errors.authjs.dev#unknownaction
at parseActionAndProviderId (~\my-app\.next\server\chunks\node_modules_@auth_core_47de2b11._.js:1178:27)
at toInternalRequest (~\my-app\.next\server\chunks\node_modules_@auth_core_47de2b11._.js:1115:40)
at Auth (~\my-app\.next\server\chunks\node_modules_@auth_core_47de2b11._.js:5331:212)
at httpHandler (~\my-app\.next\server\chunks\node_modules_bca19542._.js:4332:198)
at AsyncLocalStorage.run (node:internal/async_local_storage/async_hooks:80:14)
at AppRouteRouteModule.do (~\my-app\node_modules\next\dist\compiled\next-server\app-route-turbo.runtime.dev.js:5:38775)
at ~\my-app\node_modules\next\dist\compiled\next-server\app-route-turbo.runtime.dev.js:5:48915
at ~\my-app\node_modules\next\dist\server\lib\trace\tracer.js:170:36
at NoopContextManager.with (~\my-app\node_modules\next\dist\compiled\@opentelemetry\api\index.js:1:7062)
at ContextAPI.with (~\my-app\node_modules\next\dist\compiled\@opentelemetry\api\index.js:1:518)
at NoopTracer.startActiveSpan (~\my-app\node_modules\next\dist\compiled\@opentelemetry\api\index.js:1:18093)
at ProxyTracer.startActiveSpan (~\my-app\node_modules\next\dist\compiled\@opentelemetry\api\index.js:1:18854)
at ~\my-app\node_modules\next\dist\server\lib\trace\tracer.js:152:103
at NoopContextManager.with (~\my-app\node_modules\next\dist\compiled\@opentelemetry\api\index.js:1:7062)
at ContextAPI.with (~\my-app\node_modules\next\dist\compiled\@opentelemetry\api\index.js:1:518)
at NextTracerImpl.trace (~\my-app\node_modules\next\dist\server\lib\trace\tracer.js:152:28)
at ~\my-app\node_modules\next\dist\compiled\next-server\app-route-turbo.runtime.dev.js:5:48760
at AsyncLocalStorage.run (node:internal/async_local_storage/async_hooks:91:14)
at ~\my-app\node_modules\next\dist\compiled\next-server\app-route-turbo.runtime.dev.js:5:46097
at AsyncLocalStorage.run (node:internal/async_local_storage/async_hooks:91:14)
at ~\my-app\node_modules\next\dist\compiled\next-server\app-route-turbo.runtime.dev.js:5:46054
at AsyncLocalStorage.run (node:internal/async_local_storage/async_hooks:91:14)
at AppRouteRouteModule.handle (~\my-app\node_modules\next\dist\compiled\next-server\app-route-turbo.runtime.dev.js:5:46008)
at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
Hotfix
I resolved this with an ugly hotfix directly in the library. I used patch-package to keep this a little bit cleaner.
Create file patches/@auth+core+0.40.0.patch with this content:
diff --git a/node_modules/@auth/core/lib/utils/web.js b/node_modules/@auth/core/lib/utils/web.js
index 9f68f35..afc6059 100644
--- a/node_modules/@auth/core/lib/utils/web.js
+++ b/node_modules/@auth/core/lib/utils/web.js
@@ -89,6 +89,13 @@ export function randomString(size) {
}
/** @internal Parse the action and provider id from a URL pathname. */
export function parseActionAndProviderId(pathname, base) {
+ // HOTFIX: NextAuth.js 5 doesn't work because Next.js strips the basePath
+ if (process.env.NEXT_PUBLIC_BASE_PATH &&
+ process.env.NEXT_PUBLIC_BASE_PATH !== "/" &&
+ !pathname.startsWith(process.env.NEXT_PUBLIC_BASE_PATH)) {
+ pathname = process.env.NEXT_PUBLIC_BASE_PATH + pathname;
+ }
+ // HOTFIX END
const a = pathname.match(new RegExp(`^${base}(.+)`));
if (a === null)
throw new UnknownAction(`Cannot parse action at ${pathname}`);
Then, install patch-package and put this postinstall script in your package.json
"scripts": {
"postinstall": "patch-package"
}
I was able to get basePath working with the following workaround:
// src/app/api/auth/[...nextauth]/route.ts
import { NextRequest } from 'next/server'
import { handlers } from '@/auth/auth'
import { basePath } from '@/lib/base-route'
export async function GET(req: NextRequest) {
return injectBasePath(req, handlers.GET)
}
export async function POST(req: NextRequest) {
return injectBasePath(req, handlers.POST)
}
// Next.js removes basePath from the request, so we re-inject it to match `NextAuth({ basePath: `${basePath}/api/auth` })`
async function injectBasePath(
req: NextRequest,
handler: (req: NextRequest) => Promise<Response>,
) {
const url = new URL(req.url)
if (!url.pathname.startsWith(basePath)) {
url.pathname = `${basePath}${url.pathname}`
const modifiedReq = new NextRequest(url.toString(), req)
return handler(modifiedReq)
}
return handler(req)
}