Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for NextAuth v5 multiple middleware #596

Open
JulianJorgensen opened this issue Nov 3, 2023 · 15 comments
Open

Support for NextAuth v5 multiple middleware #596

JulianJorgensen opened this issue Nov 3, 2023 · 15 comments
Labels
area: integrations contributions welcome Good for people looking to contribute enhancement New feature or request

Comments

@JulianJorgensen
Copy link

Is your feature request related to a problem? Please describe.

I use both next-intl and next-auth middleware. There are some nice examples of this combination for next-auth v4 (withAuth()), but not for v5 when using auth.

Would love to see.

Describe the solution you'd like

I imagine the usage something like this:

import { auth } from 'auth';
import createMiddleware from 'next-intl/middleware';

const intlMiddleware = createMiddleware({
  locales: ['en', 'dk'],
  defaultLocale: 'en'
});

export default auth((req) => intlMiddleware(req))

Describe alternatives you've considered

Alternatively I can stick with using next-auth v4

@JulianJorgensen JulianJorgensen added enhancement New feature or request unconfirmed Needs triage. labels Nov 3, 2023
@amannn
Copy link
Owner

amannn commented Nov 3, 2023

I currently don't have experience with v5. We have an example within this repo as well as documentation on usage with v4.

Would you be interested in setting up a pull request with an update to v5? We have a few automated tests in example-next-13-next-auth that can help to verify if the updated code works as expected.

As far as I know, v5 is in beta currently though. We should wait until v5 becomes stable before updating our example in the main branch.

@JulianJorgensen
Copy link
Author

Sure thing! I just made a PR, but my PR skills are a bit rusty so bear with me 🙂

@amarincolas
Copy link

amarincolas commented Nov 7, 2023

I am trying to make it work but apparently, this does not work anymore

const authMiddleware = auth(
  (req) => intlMiddleware(req),
  {
    callbacks: {
      authorized: ({token}) => token != null
    },
    pages: {
      signIn: '/signin'
    }
  }
);

So I decided to follow the next-auth and next.js guidelines about this and I came up with this:

export const authConfig = {
  pages: {
    signIn: '/signin',
  },
  providers: [],
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      ...
    },
  },
} satisfies NextAuthConfig
export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        username: { type: 'text' },
        password: { type: 'password' },
      },
      authorize(credentials) {
        ...
    }),
  ],
})
const authMiddleware = auth(
  (request) => intlMiddleware(request)
)

But the thing is this does not work either...

@benjamintalou
Copy link

benjamintalou commented Nov 17, 2023

As a temporary workaround, you can set a custom header in the middleware, and handle redirection in the main layout.

In middleware.tsx

export async function middleware(request: NextRequest) {
  const intlMiddleware: (request: NextRequest) => NextResponse<unknown> = createMiddleware({
    locales: ['en', 'dk'],
    defaultLocale: 'en'
  });

  const session = await nextAuth(NextAuthConfiguration).auth();

  if (!session) {
    request.headers.set('x-auth-unauthorized', 'true');
  }

  return intlMiddleware(request);
}

In layout.tsx

  const headersList = headers();
  const unauthorized = headersList.get('x-auth-unauthorized') === 'true';

@juancmandev
Copy link

juancmandev commented Nov 20, 2023

As a temporary workaround, you can set a custom header in the middleware, and handle redirection in the main layout.

In middleware.tsx

export async function middleware(request: NextRequest) {
  const intlMiddleware: (request: NextRequest) => NextResponse<unknown> = createMiddleware({
    locales: ['en', 'dk'],
    defaultLocale: 'en'
  });

  const session = await nextAuth(NextAuthConfiguration).auth();

  if (!session) {
    request.headers.set('x-auth-unauthorized', 'true');
  }

  return intlMiddleware(request);
}

In layout.tsx

  const headersList = headers();
  const unauthorized = headersList.get('x-auth-unauthorized') === 'true';

Thanks, I'm using Supabase and I was having problems as Supabase uses a middleware too.

Here's my middleware.ts

import createMiddleware from 'next-intl/middleware';
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs';
import { NextResponse, NextRequest } from 'next/server';

export async function middleware(req: NextRequest) {
  const intlMiddleware: (request: NextRequest) => NextResponse<unknown> =
    createMiddleware({
      locales: ['en', 'es'],
      defaultLocale: 'en',
    });
  const res = NextResponse.next();
  const supabase = createMiddlewareClient({ req, res });

  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (!user && req.nextUrl.pathname.includes('account')) {
    return NextResponse.redirect(new URL('auth', req.url));
  }

  return intlMiddleware(req);
}

export const config = {
  matcher: ['/', '/(en|es)/:path*'],
};

@amannn amannn added contributions welcome Good for people looking to contribute and removed unconfirmed Needs triage. labels Nov 23, 2023
@Aw3same
Copy link

Aw3same commented Nov 28, 2023

I am trying to make it work but apparently, this does not work anymore

const authMiddleware = auth(
  (req) => intlMiddleware(req),
  {
    callbacks: {
      authorized: ({token}) => token != null
    },
    pages: {
      signIn: '/signin'
    }
  }
);

So I decided to follow the next-auth and next.js guidelines about this and I came up with this:

export const authConfig = {
  pages: {
    signIn: '/signin',
  },
  providers: [],
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      ...
    },
  },
} satisfies NextAuthConfig
export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        username: { type: 'text' },
        password: { type: 'password' },
      },
      authorize(credentials) {
        ...
    }),
  ],
})
const authMiddleware = auth(
  (request) => intlMiddleware(request)
)

But the thing is this does not work either...

Any update on this? I'm at the same point, getting this error:

image

And in the server:

⨯ node_modules\next-intl\dist\development\middleware\middleware.js (31:71) @ get
⨯ Cannot read properties of undefined (reading 'get')

The concrete line is this, if it helps
const hasOutdatedCookie = ((_request$cookies$get = request.cookies.get(constants.COOKIE_LOCALE_NAME)) === null || _request$cookies$get === void 0 ? void 0 : _request$cookies$get.value) !== locale;

UPDATE:

It seems to be a problem with the auth wrapper, because is missing the cookies see more

@alessandrojcm
Copy link

alessandrojcm commented Dec 15, 2023

What worked for me was to use the auth() overload like here and return the intl response. Like so:

const intlMiddleware = createIntlMiddleware({
  locales,
  defaultLocale,
  localePrefix: 'never', // we do not need the path since we are not worried about SEO
  localeDetection: true,
})
export default async function middleware(req: NextRequest) {
  // Init intl middleware response
  const intlResponse = intlMiddleware(req)
  // APIs handle their own auth, but they still need locale data
  if (req.nextUrl.pathname.includes('api')) {
    return intlResponse
  }
  // handle public routes...
  // private routes here
  const session = await auth()
  if(session !== null) {
    return intlResponse
  }
  // redirect to sign in if unauthorized
  const nextUrl = req.nextUrl.clone()
  nextUrl.pathname = '/auth/signin'
  // When using redirect Next.js expects a 3xx status code otherwise it errors out
  return NextResponse.redirect(nextUrl, { ...intlResponse, status: HttpCodes.PERMANENT_REDIRECT })
}

export const config = { matcher: ['/((?!_next|.*\\..*).*)'] }

Note that we are not using the locale prefix in the pathname as we do not need it, so if you do this logic might change a bit but the general idea should still work.

@wallwhite
Copy link

wallwhite commented Jan 7, 2024

There is a way how to solve the issue, maybe it's not the most elegant solution, but it works as expected:

interface AppRouteHandlerFnContext {
  params?: Record<string, string | string[]>;
}

const i18nMiddleware = createI18nMiddleware({
  locales: ['en', 'fr'],
  defaultLocale: 'en',
  urlMappingStrategy: 'rewriteDefault',
});

export const middleware = (request: NextRequest, event: AppRouteHandlerFnContext): NextResponse => {
  return NextAuth(authConfig).auth(() => {
    return i18nMiddleware(request);
  })(request, event) as NextResponse;
};

export const config = {
  matcher: [ '/', '/(en|fr)/:path*', '/((?!api|_next/static|_next/image|.png).*)'],
};

Passing the callback into auth middleware we receive not the original request object and it's a big problem because it doesn't have headers and cookies fields.

@0x963D
Copy link

0x963D commented Feb 26, 2024

This worked for me

"next": "^14.1.0",
"react": "18.2.0",
"next-auth": "5.0.0-beta.13",
"next-intl": "^3.4.2",
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
import createIntlMiddleware from "next-intl/middleware"

import { auth } from "@/auth"

import { defaultLocale, localePrefix, locales } from "./messages/config"

const publicPages = [
  "/",
  "/unauthorized",
  "/pricing",
  "/roadmap",
  "/features",
  "/contact",
  "/about",
  "/help",
  "/privacy-policy",
  "/terms-of-service",
  "/cookie-policy",
  "/end-user-license-agreement"
]

const authPages = ["/sign-in", "/sign-up"]

const testPathnameRegex = (pages: string[], pathName: string): boolean => {
  return RegExp(
    `^(/(${locales.join("|")}))?(${pages.flatMap((p) => (p === "/" ? ["", "/"] : p)).join("|")})/?$`,
    "i"
  ).test(pathName)
}

const intlMiddleware = createIntlMiddleware({
  locales,
  defaultLocale,
  localePrefix
})

const authMiddleware = auth((req) => {
  const isAuthPage = testPathnameRegex(authPages, req.nextUrl.pathname)
  const session = req.auth

  // Redirect to sign-in page if not authenticated
  if (!session && !isAuthPage) {
    return NextResponse.redirect(new URL("/sign-in", req.nextUrl))
  }

  // Redirect to home page if authenticated and trying to access auth pages
  if (session && isAuthPage) {
    return NextResponse.redirect(new URL("/", req.nextUrl))
  }

  return intlMiddleware(req)
})

const middleware = (req: NextRequest) => {
  const isPublicPage = testPathnameRegex(publicPages, req.nextUrl.pathname)
  const isAuthPage = testPathnameRegex(authPages, req.nextUrl.pathname)

  if (isAuthPage) {
    return (authMiddleware as any)(req)
  }

  if (isPublicPage) {
    return intlMiddleware(req)
  } else {
    return (authMiddleware as any)(req)
  }
}

export const config = {
  matcher: ["/((?!api|_next|.*\\..*).*)"]
}

export default middleware

@kisstamasj
Copy link

I found this article wich is about to chain multiple middleware. I didn't try it yet, but what do you think about it, i think this would be the greate solution, to chain up multiple middlewares so you can extend your application as you need.

https://reacthustle.com/blog/how-to-chain-multiple-middleware-functions-in-nextjs

@neo-rob-little
Copy link

neo-rob-little commented Jun 20, 2024

@0x963D This is great thank you!

Not specific to this code necessarily I am trying this out with next-auth v5 on with next-intl on aws amplify and having the most odd issue, this code works great does the redirection correctly but the second I hit a localized route where next-intl middleware is providing the NextResponse I get into a redirect loop where it requests the same page over and over again. It's not a specific route but any localized route returned by next-intl middleware either before auth or after successful auth doesn't seem to matter matter.

As soon as I get a response from next-intl it's massive 307 spam as the middleware redirects the current page to itself. Did I do something wrong here in the code to get into this loop? or is this an amplify thing?

next@14.2.4
next-intl@3.15.2
edit: this issue appears to be caused by the next-auth@beta middleware doing in conjunction with next-intl perhaps something to the headers or request (NextRequest) and not next-intl.

import { NextRequest, NextResponse } from "next/server"
import { auth } from "auth"
import createMiddleware from "next-intl/middleware"

import { locales } from "@/config/locales"

import { localePrefix, pathnames } from "./navigation" 
/*
 * Behind the scenes, NextAuth.js uses Next.js API Routes to handle requests to /api/auth/* and provides the following routes:
 * GET /api/auth/signin
 * POST /api/auth/signin/:provider
 * GET/POST /api/auth/callback/:provider
 * GET /api/auth/signout
 * POST /api/auth/signout
 * GET /api/auth/session
 * GET /api/auth/csrf
 * GET /api/auth/providers
 */

// Define the public pages that should be accessible without authentication
const publicPages = ["/help"]

const authPages = ["/login", "/sign-up"]

const testPathnameRegex = (pages: string[], pathName: string): boolean => {
  return RegExp(
    `^(/(${locales.join("|")}))?(${pages.flatMap((p) => (p === "/" ? ["", "/"] : p)).join("|")})/?$`,
    "i"
  ).test(pathName)
}

const intlMiddleware = createMiddleware({
  // A list of all locales that are supported
  locales,
  pathnames,
  localePrefix,
  // If this locale is matched, pathnames work without a prefix (e.g. `/about`)
  defaultLocale: "en-US",
  localeDetection: true,
})

const authMiddleware = auth((req) => {
  const isAuthPage = testPathnameRegex(authPages, req.nextUrl.pathname)
  const session = req.auth

  // Redirect to sign-in page if not authenticated
  if (!session && !isAuthPage) {
    return NextResponse.redirect(new URL("/login", req.nextUrl))
  }

  // Redirect to home page if authenticated and trying to access auth pages
  if (session && isAuthPage) {
    return NextResponse.redirect(new URL("/", req.nextUrl))
  }

  return intlMiddleware(req)
})

// https://github.com/vercel/next.js/blob/canary/examples/with-strict-csp/middleware.js#L4
// allow access if the nonce in the content security policy matches the x-nonce header

// script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
// style-src 'self' 'nonce-${nonce}';
const middleware = (request: NextRequest) => {
  // // allow public access to next-auth routes for signin, signout, etc.
  if (request.nextUrl.pathname.startsWith("/auth")) {
    return NextResponse.next({
      // headers: requestHeaders,
    })
  }

  const isAuthPage = testPathnameRegex(authPages, request.nextUrl.pathname)
  const isPublicPage = testPathnameRegex(publicPages, request.nextUrl.pathname)

  if (isAuthPage) {
    return (authMiddleware as any)(request)
  }

  if (isPublicPage) {
    return intlMiddleware(request)
  } else {
    return (authMiddleware as any)(request)
  }
}

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)", "/"],
}

export default middleware

image

@fkysly
Copy link

fkysly commented Jul 25, 2024

the key point is next-intl must use original request, not auth req:

import { auth } from '@/auth';
import createIntlMiddleware from 'next-intl/middleware';
import { defaultLocale, localePrefix, locales } from '@/lib/i18n/i18nConfig';
import { NextRequest, NextResponse } from 'next/server';

interface AppRouteHandlerFnContext {
  params?: Record<string, string | string[]>;
}

const intlMiddleware = createIntlMiddleware({
  locales,
  defaultLocale,
  localePrefix
});

const homePages = ['/', '/signin'];
const authPages = [
  '/dashboard',
  '/dashboard/post',
  '/dashboard/analytics',
  '/dashboard/followers'
];

const getPathnameRegex = (pages: string[]) =>
  RegExp(
    `^(/(${locales.join('|')}))?(${pages
      .flatMap((p) => (p === '/' ? ['', '/'] : p))
      .join('|')})/?$`,
    'i'
  );

const homePathnameRegex = getPathnameRegex(homePages);
const authPathnameRegex = getPathnameRegex(authPages);

const authMiddleware = (
  request: NextRequest,
  ctx: AppRouteHandlerFnContext
) => {
  return auth((req) => {
    const path = req.nextUrl.pathname;
    const isAuth = req.auth;

    const isHomePage = homePathnameRegex.test(path);
    const isAuthPage = authPathnameRegex.test(path);

    if (isAuth && isHomePage) {
      return NextResponse.redirect(new URL('/dashboard', req.url));
    } else if (!isAuth) {
      if (isAuthPage) {
        return NextResponse.redirect(new URL('/signin', req.url));
      }
    }

    return intlMiddleware(request);
  })(request, ctx);
};

export const middleware = (
  request: NextRequest,
  ctx: AppRouteHandlerFnContext
): NextResponse => {
  if (request.nextUrl.pathname.startsWith('/auth')) {
    return NextResponse.next();
  }

  return authMiddleware(request, ctx) as NextResponse;
};

export const config = {
  matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
};

@nonoakij
Copy link

@fkysly I have seen the code of the implementation. Thank you so much.

I do have one question, though. According to the Next.js documentation, the second argument of the middleware should be NextFetchEvent.
https://nextjs.org/docs/app/building-your-application/routing/middleware#waituntil-and-nextfetchevent

In your implementation, however, it is AppRouteHandlerFnContext, and it seems that the TypeScript types are not compatible.

Could you please clarify if this adjustment was made to align with the auth function’s type definition? Is this a safe assumption?

@hugocruzlfc
Copy link

I have had the same problem. And it has been a complex thing to solve. In my case I used this example:

https://github.com/hugocruzlfc/nextjs-with-authjs-and-next-intl

As a basis, I even followed the same pattern for my current company, including Prisma.

The headache is introduced by the middleware configuration. I hope it helps.

Good luck to you.

@pGabrielM
Copy link

pGabrielM commented Oct 21, 2024

I have had the same problem. And it has been a complex thing to solve. In my case I used this example:

https://github.com/hugocruzlfc/nextjs-with-authjs-and-next-intl

As a basis, I even followed the same pattern for my current company, including Prisma.

The headache is introduced by the middleware configuration. I hope it helps.

Good luck to you.

I used your next-intl setup but when i invoke getNow() or getLocale() methods in server side of my app i recieve the error bellow:

Error:

Unable to find next-intl locale because the middleware didn't run on this request. See https://next-intl-docs.vercel.app/docs/routing/middleware#unable-to-find-locale. The notFound() function will be called as a result.

middleware.ts

import { auth } from '@/auth'
import createMiddleware from 'next-intl/middleware'
import { NextMiddleware, NextRequest, NextResponse } from 'next/server'

import { locales, routing } from './i18n/routing'

const intlMiddleware = createMiddleware(routing)

const publicRoutes = [
  '/',
  '/projects/[id]',
  '/register',
  '/forgot-password',
  '/recover-password',

  '/',
  '/projetos/[id]',
  '/registrar',
  '/esqueci-senha',
  '/recuperar-senha',
]

const authRoutes = ['/login']

const testPathnameRegex = (pages: string[], pathName: string): boolean => {
  // Replace dynamic routes with regex
  const pathsWithParams = pages.map((p) => p.replace(/\[.*?\]/g, '[^/]+'))

  return RegExp(
    `^(/(${locales.join('|')}))?(${pathsWithParams.flatMap((p) => (p === '/' ? ['', '/'] : p)).join('|')})/?$`,
    'i',
  ).test(pathName)
}

const authMiddleware = auth((req) => {
  const isAuthPage = testPathnameRegex(authRoutes, req.nextUrl.pathname)
  const isLogged = !!req.auth

  // Redirect to login page if not authenticated
  if (!isLogged && !isAuthPage) {
    return NextResponse.redirect(new URL('/login', req.nextUrl))
  }

  // Redirect to home page if authenticated and trying to access auth pages
  if (isLogged && isAuthPage) {
    return NextResponse.redirect(new URL('/', req.nextUrl))
  }

  return intlMiddleware(req)
})

const middleware = (req: NextRequest): NextMiddleware | NextResponse => {
  const isPublicPage = testPathnameRegex(publicRoutes, req.nextUrl.pathname)
  const isAuthPage = testPathnameRegex(authRoutes, req.nextUrl.pathname)

  if (isAuthPage) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    return (authMiddleware as any)(req)
  }

  if (isPublicPage) {
    return intlMiddleware(req)
  } else {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    return (authMiddleware as any)(req)
  }
}

export const config = {
  matcher: [
    '/((?!api|_next|_vercel|.*\\..*).*)'
  ],
}

export default middleware

I tried a lot of things to solve but i cant find the problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: integrations contributions welcome Good for people looking to contribute enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

16 participants