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

in the middleware, the session is not extended with custom fields #9836

Open
SebastianNarvaez11 opened this issue Jan 28, 2024 · 43 comments
Open
Labels
documentation Relates to documentation

Comments

@SebastianNarvaez11
Copy link

Environment

System:
OS: Windows 11 10.0.22631
CPU: (12) x64 AMD Ryzen 5 3600 6-Core Processor
Memory: 7.60 GB / 15.95 GB
Binaries:
Node: 20.11.0 - C:\Program Files\nodejs\node.EXE
Yarn: 1.22.19 - C:\Program Files (x86)\Yarn\bin\yarn.CMD
npm: 10.2.4 - C:\Program Files\nodejs\npm.CMD
Browsers:
Edge: Chromium (120.0.2210.91)
Internet Explorer: 11.0.22621.1
npmPackages:
@auth/prisma-adapter: ^1.1.0 => 1.1.0
next: 14.1.0 => 14.1.0
next-auth: 5.0.0-beta.5 => 5.0.0-beta.5
react: ^18 => 18.2.0

Reproduction URL

https://github.com/SebastianNarvaez11/TesloShop

Describe the issue

I am trying to get the custom field "role" in the middleware using req.auth but the custom fields that I added when creating the session previously are not there

In middleware.ts console.log(req.auth) :
image

but with use auth() in the component it works :
const session = await auth(); console.log(session) :
image

How to reproduce

My file src/auth.ts :
image

My file src/auth.config.ts:
image

My file src/middleware.ts, the problem is here:
image

Expected behavior

In the middleware you should have access to the custom fields that you added when creating the session

@SebastianNarvaez11 SebastianNarvaez11 added bug Something isn't working triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime. labels Jan 28, 2024
@deltasierra96
Copy link

I too am facing this issue. The middleware's the only place I'm not having custom fields returned.

Have you been able to find a fix?

@SebastianNarvaez11
Copy link
Author

@deltasierra96 Nothing 😔

@ConnorC18
Copy link

ConnorC18 commented Feb 4, 2024

@SebastianNarvaez11 @deltasierra96

I also faced this issue, what seems to have worked for me was combining my NextAuth instantiations.

The Fix

You have called NextAuth(...) in src/middleware.ts and src/auth.ts using two different configurations. I was doing a similar thing. What you will want to do is move the content of the NextAuth(...) instantiation in src/auth.ts into src/auth.config.ts so that you have one main config with all your params for NextAuth(...).

Your src/auth.ts should now look a little something like:

import NextAuth from "next-auth";
import authConfig from "@/auth.config";

export const {
  handlers: { GET, POST },
  auth,
  signIn,
  signOut,
} = NextAuth(authConfig);

Your src/auth.config.ts should now have ALL your configuration in it:

import type { NextAuthConfig } from "next-auth";
// ... Prisma Imports

export default {
  providers: [...],
  pages: {
    signIn: "/auth/login",
    error: "/auth/error",
  },
  session: { strategy: "jwt" },

  callbacks: {
    authorized({ request, auth }) {
      console.log("Please Remove Me. This is a POC", auth) // <-- This should have your additional user data!
      return true;
    },
    async session({ token, session }) {
      if (token?.data?.role && session.user) {
        session.user.role = token.data.role;
      }

      return session;
    },
    async jwt({ token }) {
      // ... Your Prisma Code

      token.data = user    

      return token;
    },
  },
} satisfies NextAuthConfig;

This should resolve the issue. I have freehand typed this in GitHub so this code will probably not be perfect and will almost 100% have at least 1 error, but the idea should be there enough to hopefully help the issue.


The (assumed) Root Cause

It seems the actual cause is because the instantiations are different, calling NextAuth(...) in src/middleware.ts and src/auth.ts works perfectly fine after moving all the configuration settings into one file. Thinking logically now (after rage researching all day over not being able to find the solution to this) it makes perfect sense as in the example you provided (very similar to mine) the middleware would have no idea that the session needs manipulating as the session callback is not in the config being used in src/middleware.ts

TL;DR

You need to use one configuration/NextAuth instantiation for both your providers & middleware

  • Make one central auth config
  • Only call NextAuth once and export all the returned objects
  • Import "auth" from the previous step into the middleware
  • You should now see the role in your middleware auth user data.

Just follow the full fix, its not too long...


I hope that fixes your issue - If this is the problem for you, it seems the problem is NOT with next-auth. It may be worth documenting this somewhere though as its (IMO) an easy mistake to make!

@wizzyto12
Copy link

@SebastianNarvaez11 @deltasierra96

I also faced this issue, what seems to have worked for me was combining my NextAuth instantiations.

The Fix

You have called NextAuth(...) in src/middleware.ts and src/auth.ts using two different configurations. I was doing a similar thing. What you will want to do is move the content of the NextAuth(...) instantiation in src/auth.ts into src/auth.config.ts so that you have one main config with all your params for NextAuth(...).

Your src/auth.ts should now look a little something like:

import NextAuth from "next-auth";
import authConfig from "@/auth.config";

export const {
  handlers: { GET, POST },
  auth,
  signIn,
  signOut,
} = NextAuth(authConfig);

Your src/auth.config.ts should now have ALL your configuration in it:

import type { NextAuthConfig } from "next-auth";
// ... Prisma Imports

export default {
  providers: [...],
  pages: {
    signIn: "/auth/login",
    error: "/auth/error",
  },
  session: { strategy: "jwt" },

  callbacks: {
    authorized({ request, auth }) {
      console.log("Please Remove Me. This is a POC", auth) // <-- This should have your additional user data!
      return true;
    },
    async session({ token, session }) {
      if (token?.data?.role && session.user) {
        session.user.role = token.data.role;
      }

      return session;
    },
    async jwt({ token }) {
      // ... Your Prisma Code

      token.data = user    

      return token;
    },
  },
} satisfies NextAuthConfig;

This should resolve the issue. I have freehand typed this in GitHub so this code will probably not be perfect and will almost 100% have at least 1 error, but the idea should be there enough to hopefully help the issue.

The (assumed) Root Cause

It seems the actual cause is because the instantiations are different, calling NextAuth(...) in src/middleware.ts and src/auth.ts works perfectly fine after moving all the configuration settings into one file. Thinking logically now (after rage researching all day over not being able to find the solution to this) it makes perfect sense as in the example you provided (very similar to mine) the middleware would have no idea that the session needs manipulating as the session callback is not in the config being used in src/middleware.ts

TL;DR

You need to use one configuration/NextAuth instantiation for both your providers & middleware

  • Make one central auth config
  • Only call NextAuth once and export all the returned objects
  • Import "auth" from the previous step into the middleware
  • You should now see the role in your middleware auth user data.

Just follow the full fix, its not too long...

I hope that fixes your issue - If this is the problem for you, it seems the problem is NOT with next-auth. It may be worth documenting this somewhere though as its (IMO) an easy mistake to make!

THIS ABSOLUTELY WORKS! Lets hope they either document it or fix it.

@AmphibianDev
Copy link

AmphibianDev commented Feb 8, 2024

But with adapter: PrismaAdapter(prisma) when I move the callbacks: {...} from auth.ts to auth.config.ts I got the error [auth][cause]: Error: PrismaClient is unable to run in Vercel Edge Functions or Edge Middleware. As an alternative, try Accelerate: https://pris.ly/d/accelerate. Am I forced to use Accelerate to resolve this issue?

@ThangHuuVu ThangHuuVu added documentation Relates to documentation and removed bug Something isn't working triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime. labels Feb 8, 2024
@ThangHuuVu
Copy link
Member

Thanks for reporting, and thanks @ConnorC18 for explaining, that is exactly what we recommend at the moment. Some of us are working on a new doc to address this use-case, cc @ndom91 @ubbe-xyz 🙏

@ThangHuuVu
Copy link
Member

@AmphibianDev I think you can define the adapter in the auth.ts file instead of the auth.config.ts file. See an example here in our dev app:

// adapter: PrismaAdapter(globalThis.prisma),

@ndom91
Copy link
Member

ndom91 commented Feb 8, 2024

But with adapter: PrismaAdapter(prisma) when I move the callbacks: {...} from auth.ts to auth.config.ts I got the error [auth][cause]: Error: PrismaClient is unable to run in Vercel Edge Functions or Edge Middleware. As an alternative, try Accelerate: https://pris.ly/d/accelerate. Am I forced to use Accelerate to resolve this issue?

Prisma doesn't support edge runtimes yet though, that's right.

In their most recent change log for 5.9.0 they mentioned bringing out support for it (outside of accelerate). So hopefully it'll here soon 🤞

Thanks for the ping though thang. Definitely something we have to be clear about in the docs 👍

@AmphibianDev
Copy link

AmphibianDev commented Feb 8, 2024

@ThangHuuVu

@AmphibianDev, I think you can define the adapter in the auth.ts file instead of the auth.config.ts file. See an example here in our dev app:

I already did; here is my repo: https://github.com/AmphibianDev/todo-app.
The only thing that I am missing is the user role-based routing in the middleware.ts, and the project is done. But I can't do that because req.auth?.user?.role is always undefined.

@ndom91, are you saying it's impossible for me to get the user custom fields user.role in the middleware without using the edge runtime? I want to deploy my Next.js app on a VPS together with PostgreSQL, so I want to stay away from the edge.

I'm quite new to Next.js and next-auth, so I started with a simple ToDo App to get the hang of things. Sorry to bother, but I have one more question, if I may: If I protect a route in the auth((req) => {...}); middleware, for example, if (req.nextUrl.pathname.includes("/protected")) return Response.redirect(new URL("/", req.nextUrl));, do I still need to protect the server actions that are run in the "/protected" page.tsx?

@ndom91
Copy link
Member

ndom91 commented Feb 8, 2024

@AmphibianDev ah okay, no so only if you're running on an edge runtime then Prisma has issues, although they can be worked around like implied by Thang and ConnorC18 detailed. "slef hosting" the Next.js app on a VPS should be fine. Make sure to use @prisma/client@5.9.1, they just released a little fix that makes this better.

Regarding your second question the mdidleware runs in front of every request (unless you white / blacklist something in the "matchers" setting), so your server actions should be covered 👍

@AmphibianDev
Copy link

AmphibianDev commented Feb 8, 2024

only if you're running on an edge runtime then Prisma has issues, although they can be worked around like implied by Thang and ConnorC18 detailed.

@ndom91, I did like ConnorC18 and Thang implied, but I got the error:

[auth][error] JWTSessionError: Read more at https://errors.authjs.dev#jwtsessionerror
[auth][cause]: Error: PrismaClient is unable to run in Vercel Edge Functions or Edge Middleware. As an alternative, try Accelerate: https://pris.ly/d/accelerate.
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report
    at Object.get (C:\Users\Work\Desktop\todo-app\.next\server\edge\chunks\node_modules_4328cd._.js:4744:23)
    at Object.jwt (C:\Users\Work\Desktop\todo-app\.next\server\edge\chunks\_f42337._.js:147:153)
    at Module.session (C:\Users\Work\Desktop\todo-app\.next\server\edge\chunks\node_modules_d0ec2d._.js:3091:43)
    at async Module.AuthInternal (C:\Users\Work\Desktop\todo-app\.next\server\edge\chunks\node_modules_d0ec2d._.js:3582:24)
    at async Module.Auth (C:\Users\Work\Desktop\todo-app\.next\server\edge\chunks\node_modules_d0ec2d._.js:3722:29)
    at async handleAuth (C:\Users\Work\Desktop\todo-app\.next\server\edge\chunks\node_modules_4328cd._.js:3120:29)
    at async Module.adapter (C:\Users\Work\Desktop\todo-app\.next\server\edge\chunks\_540240._.js:6699:16)
    at async runWithTaggedErrors (C:\Users\Work\Desktop\todo-app\node_modules\next\dist\server\web\sandbox\sandbox.js:99:24)
    at async DevServer.runMiddleware (C:\Users\Work\Desktop\todo-app\node_modules\next\dist\server\next-server.js:1039:24)
    at async DevServer.runMiddleware (C:\Users\Work\Desktop\todo-app\node_modules\next\dist\server\dev\next-dev-server.js:261:28)
[auth][details]: {}

If I move the callbacks: {...} or the adapter: PrismaAdapter(prisma) from auth.js into the auth.config.js I go to the error. And as I understand, I need to move the callbacks: {...} into the auth.config.js to have the user custom fields in the middleware auth((req) => {...});. Did I got it wrong?

@ConnorC18
Copy link

ConnorC18 commented Feb 8, 2024

@AmphibianDev make sure you are not calling prisma in the middleware.ts handler. I don't believe this error is caused by the PrismaAdapter; the PrismaClient causes it. Just make sure you are not using prisma in the middleware

@ConnorC18
Copy link

@AmphibianDev

If I move the callbacks: {...} or the adapter: PrismaAdapter(prisma) from auth.js into the auth.config.js

Also make sure you are copying over session: { strategy: "jwt" }, into the auth.config.js as not having this in the config while adapter: PrismaAdapter(prisma) is present might also cause this issue.

@AmphibianDev
Copy link

AmphibianDev commented Feb 8, 2024

@ConnorC18 Believe me, I have tried every combination of moves possible. 😓
The only working one, but with no user custom filed in the middleware is: {pages, callbacks, adapter, session, jwt, ...authConfig} in auth.ts and {providers} in auth.config.ts
I have tried:

  1. auth.ts {...authConfig}, auth.config.ts {pages, callbacks, adapter, session, jwt, providers}
  2. auth.ts {pages, adapter, ...authConfig}, auth.config.ts {callbacks, session, jwt, providers}
  3. ... 4. ... 5. ... 😭

I have removed any auth check from my app and made it easier to spin up: https://github.com/AmphibianDev/todo-app
You just need to remove the .example from .env.example and: npm install, docker compose up -d, npx prisma db push and for the login can be anything like a@a.com and 1234 for the 2FA.

I want to make this work so bad... 😞

@jainmohit2001
Copy link

jainmohit2001 commented Feb 9, 2024

Hi everyone.
Here's a thumb rule you can follow:

  1. Define all config in a single place - auth.config.ts
  2. In auth.ts export the following:
export const { handlers, auth, signIn, signOut } = NextAuth({...authConfig})
  1. Use the exported handlers in /api/auth/[...nextAuth]/app.ts.

All is good until you want to add middleware.ts.

Now you should be able to use the same exported auth from auth.ts but since there are adapters that are not compatible with middleware, you cannot use them YET. I am trying to use postgres with drizzle-orm and at the time of writing this, only neon serverless has a postgres connector.

How did I resolve this?

I moved the adapter and any callbacks requiring DB access into auth.ts.
I had to reinitialize NextAuth in middleware as follows:

import NextAuth from 'next-auth'

import { authConfig } from './auth.config'

const auth = NextAuth(authConfig).auth

export default auth((req) => {
// Do some routing stuff here if you want
// The `req.auth` will have all the fields that you returned via the `session` callback
})

Use the above hack until the adapters support running on edge.
BTW - I spent 2 days setting up the Next Auth v5. I finally completed it. Here are my thoughts:

  1. The current documentation state is not good. Refer to this video for any setup-related issues: https://www.youtube.com/watch?v=1MTyCvS05V4&t=15629s. Shoutout to whoever created this.
  2. After setup, things are much simpler and easier to handle.
  3. I even created my adapter because of my requirements using drizzle.
  4. AdapterModels should be implemented in a way that we can override stuff. I needed my userId to be an integer, which is not possible at all without raising a bunch of errors in NextAuth. Used the cal.com GitHub repo to understand how to do this.

@wizzyto12
Copy link

Hi everyone. Here's a thumb rule you can follow:

  1. Define all config in a single place - auth.config.ts
  2. In auth.ts export the following:
export const { handlers, auth, signIn, signOut } = NextAuth({...authConfig})
  1. Use the exported handlers in /api/auth/[...nextAuth]/app.ts.

All is good until you want to add middleware.ts.

Now you should be able to use the same exported auth from auth.ts but since there are adapters that are not compatible with middleware, you cannot use them YET.

How did I resolve this?

I moved the adapter and any callbacks requiring DB access into auth.ts. I had to reinitialize NextAuth in middleware as follows:

import NextAuth from 'next-auth'

import { authConfig } from './auth.config'

const auth = NextAuth(authConfig).auth

export default auth((req) => {
// Do some routing stuff here if you want
// The `req.auth` will have all the fields that you returned via the `session` callback
})

Use the above hack until the adapters support running on edge. BTW - I spent 2 days setting up the Next Auth v5. I finally completed it. Here are my thoughts:

  1. The current documentation state is not good. Refer to this video for any setup-related issues: https://www.youtube.com/watch?v=1MTyCvS05V4&t=15629s. Shoutout to whoever created this.
  2. After setup, things are much simpler and easier to handle.
  3. I even created my adapter because of my requirements using drizzle.
  4. AdapterModels should be implemented in a way that we can override stuff. I needed my userId to be an integer, which is not possible at all without raising a bunch of errors in NextAuth. Used the cal.com GitHub repo to understand how to do this.

Im currently battleing this behaviour too, fighting it for 6 days now. Can you please share your code so we can see what was done in order to get this going? I have tried using prisma - only with accelerate which is paid, drizzle - "edgeFunction is not defined".

@AmphibianDev
Copy link

AmphibianDev commented Feb 9, 2024

// The req.auth will have all the fields that you returned via the session callback

@jainmohit2001 I don't get it, I tried following what you said, but they still don't have all the fields returned from the session callback, in the middleware.

middleware.ts

import NextAuth from "next-auth";
import authConfig from "@/auth.config";

const { auth } = NextAuth(authConfig);

export default auth((req) => {
  console.log(req.auth?.user); // => only { email: 'myemail@gmail.com' }
});

export const config = {
  matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
};

auth.ts

import NextAuth from "next-auth";
import authConfig from "@/auth.config";
import prisma from "@/lib/prisma";
import { PrismaAdapter } from "@auth/prisma-adapter";

export const {
  handlers: { GET, POST },
  auth,
  signIn,
  signOut,
} = NextAuth({
  callbacks: {
    async session({ session, token }) {
      if (token.sub && session.user) {
        session.user.id = token.sub;
        session.user.role = token.role;
      }

      return session;
    },
    async jwt({ token }) {
      if (!token.sub) return token;

      const user = await prisma.user.findUnique({
        where: { id: token.sub },
      });

      if (!user) return token;

      token.role = user.role;

      return token;
    },
  },
  adapter: PrismaAdapter(prisma),
  ...authConfig,
});

auth.config.ts

import { NextAuthConfig } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { $LogInSchema } from "./lib/validation";
import prisma from "@/lib/prisma";
import { User as PrismaUser, UserRole } from "@prisma/client";

import { JWT } from "next-auth/jwt";
declare module "next-auth/jwt" {
  interface JWT {
    role: UserRole;
  }
}

declare module "next-auth" {
  interface User extends PrismaUser {}
}

export default {
  session: { strategy: "jwt" },
  pages: {
    signIn: "/auth/login",
    error: "/auth/error",
  },
  providers: [
    CredentialsProvider({
      credentials: {
        email: { label: "Email", type: "email" },
        phone: { label: "Phone", type: "tel" },
      },
      async authorize(credentials, req) {
        const validatedFields = $LogInSchema.safeParse(credentials);

        if (validatedFields.success) {
          const { email, phone } = validatedFields.data;
          const user = await prisma.user.findFirst({
            where: {
              OR: [{ email }, { phone }],
            },
          });

          if (!user) return null;

          return user;
        }

        return null;
      },
    }),
  ],
} satisfies NextAuthConfig;

@JorgeMadson
Copy link

JorgeMadson commented Feb 9, 2024

Guys sorry to bother but how about my case?
I am doing a subdomain in my middleware.

export default NextAuth(authConfig).auth(middleware);

async function middleware(req: NextRequest) {

  const url = req.nextUrl;
  const hostname = getHostname(req.headers.get("host")!);
  const path = getPath(url.pathname, url.searchParams.toString());

  const isBusSearch = isBusSearchPath(path);
  if (isBusSearch) {
    const [, origin, destination] = isBusSearch;
    return NextResponse.redirect(`${MONOLITH_LINK}/onibus/${origin}/${destination}`);
  }

  if (shouldRewriteRootApplication(hostname)) {
    const rewrittenUrl = getRewrittenUrl("/home", path, new URL(req.url));
    return NextResponse.rewrite(rewrittenUrl);
  }

  const subdomain = getSubdomain(hostname);
  if (subdomain !== "www") {
    const rewrittenUrl = getRewrittenUrl(`/${subdomain}`, path, new URL(req.url));
    return NextResponse.rewrite(rewrittenUrl);
  }
}

function getRewrittenUrl(basePath: string, path: string, reqUrl: URL): URL {
  return new URL(`${basePath}${path === "/" ? "" : path}`, reqUrl);
}

The problem is the auth seems to run before the middleware.

@jainmohit2001
Copy link

Hi @AmphibianDev.
A good starting point would be to turn on the logger for the NextAuth by providing a logger param and see the debug output.

It seems like you are using CredentialsProvider. I am afraid I won't be able to tell the issue for this. I am using EmailProvider.

I can point out that the jwt and session callbacks are required in auth.config.ts as well. Please try adding those callbacks and console.log whatever you are getting and see if the required data is present or not.

@AmphibianDev
Copy link

@jainmohit2001 You gave me hope again, I was literally just writing about giving up, and your message popped up, I even started to have PTSD when I opened the auth's files 😭
I added the session and jwt to both the auth.ts and auth.config.ts, no difference. And how do I turn on the logger? I only found this:

import log from "logging-service" //-> Cannot find module 'logging-service' or its corresponding type declarations.ts(2307)

export default NextAuth({
  logger: {
    error(code, ...message) {
      log.error(code, message)
    },
    warn(code, ...message) {
      log.warn(code, message)
    },
    debug(code, ...message) {
      log.debug(code, message)
    }
  }
})

And I added the below code into the auth.ts, but I got nothing in the console.

NextAuth({
  debug: true,
  logger: {
    error(code, ...message) {
      console.log(`${code}  ${message}`);
    },
    warn(code, ...message) {
      console.log(`${code}  ${message}`);
    },
    debug(code, ...message) {
      console.log(`${code}  ${message}`);
    },
  },
...

@ConnorC18
Copy link

ConnorC18 commented Feb 9, 2024

@AmphibianDev I think this is almost 100% a workaround, but it seems to work and is why my one currently works. If you are running it in a non-edge environment, I think this approach is fine.

To check this actually works, I've made the changes and made a PR for this. Others can check the code here for an easier to read breakdown of the required changes but the explanation below might be useful

Fix PR ...For AmphibianDev

AmphibianDev/todo-app#1

auth.config.ts

  import prisma from "@/lib/prisma";
  import { NextAuthConfig } from "next-auth";
  import CredentialsProvider from "next-auth/providers/credentials";

  import { $LogInSchema } from "./lib/validation";
+ import { getUserById } from "./data/user";
+ import { PrismaAdapter } from "@auth/prisma-adapter";

  export default {
    providers: [
      // Your provider stuff from before... Nothing has changed here
    ],
+   // We have moved the callbacks into this file
    callbacks: {
      async session({ session, token }) {
        if (token.sub && session.user) {
          session.user.id = token.sub;
        }

        if (session.user) {
          session.user.id = token.sub;
          session.user.role = token.role;
        }

        return session;
      },
      async jwt({ token }) {
        if (!token.sub) return token;

-       const user = await prisma.user.findUnique({
-         where: { id: token.sub },
-       });

+       const user = await getUserById(token.sub);

        if (!user) return token;

        token.role = user.role;

        return token;
      },
    },
+   // We have moved the session into this file
    session: { strategy: "jwt", maxAge: 365 * 24 * 60 * 60 },
+   // We have moved the jwt into this file
    jwt: { maxAge: 365 * 24 * 60 * 60 },
+   // We have moved the adapter into this file
    adapter: PrismaAdapter(prisma),
  } satisfies NextAuthConfig;

auth.ts

import NextAuth from "next-auth";
import authConfig from "@/auth.config";

export const {
  handlers: { GET, POST },
  auth,
  signIn,
  signOut,
} = NextAuth({
  pages: {
    signIn: "/auth/login",
    error: "/auth/error",
  },
  ...authConfig,
});

middleware.ts

//... Other Imports
import NextAuth from "next-auth";
import authConfig from "@/auth.config";
const { auth } = NextAuth(authConfig);

export default auth((req) => {
  const { nextUrl } = req;
  const pathname = nextUrl.pathname;

  const isLoggedIn = !!req.auth;

  const isAdmin = req.auth?.user?.role === "ADMIN";
  console.log(req.auth?.user); // => { email: 'myemail@gmail.com' }
//... The rest of your code here
})

/data/user.ts

import prisma from "@/lib/prisma";

export const getUserById = async (id: string) => {
  try {
    const user = await prisma.user.findUnique({ where: { id } });

    return user;
  } catch {
    return null;
  }
};

So whats the workaround?

Basically it looks like the workaround is to import a function that calls prisma independently and then just await that function. Doing this seems to solve the issue. If this is a valid work around this 100% needs documenting, and if it is, it really needs to be made clearer.

@jainmohit2001
Copy link

jainmohit2001 commented Feb 9, 2024

Hi @ConnorC18. Thanks for helping out @AmphibianDev with the PR. Appreciate it!!

A quick question:
Is Prisma actually running on middleware using this approach? I have my doubts since Prisma is not edge-compatible. Are you able to build the project using npm run build ?

@ConnorC18
Copy link

ConnorC18 commented Feb 9, 2024

Hi @ConnorC18. Thanks for helping out @AmphibianDev with the PR. Appreciate it!!

A quick question: Is Prisma actually running on middleware using this approach? I have my doubts since Prisma is not edge-compatible. Are you able to build the project using npm run build ?

No worries! So, I am exclusively looking at this from a Non Edge standpoint, as that is what myself and @AmphibianDev are both using in this case. As far as I can see, it builds fine and runs (with the exception of the session callback having a es-line type issue in @AmphibianDev s case, but I believe that is because he needs to update his next-auth version, as I do not have this issue with the same approach). After building, the following successful output can be seen for @AmphibianDev s project

image

@jainmohit2001
Copy link

jainmohit2001 commented Feb 9, 2024

Well seems like Prisma can work in edge runtime. I am using postgres package with drizzle-orm so no luck. Here's an issue that might be of use if you ever come across this in future vercel/next.js#28774 (comment)

Edit: I am also using a DB adapter! Needed in EmailProvider.

@jainmohit2001
Copy link

image

Here's an image thats keeping me sane about the whole authentication system. I really miss those Django days.

@AmphibianDev
Copy link

Big thanks to ConnorC18 for the amazing help! ⭐⭐⭐⭐⭐

es-line type issue in @AmphibianDev s case, but I believe that is because he needs to update his next-auth version

./auth.config.ts:42:30
Type error: Property 'token' does not exist on type '({ session: Session; user: AdapterUser; } | { session: Session; token: JWT; }) & { newSession: any; trigger?: "update" | undefined; }'.

  40 |   ],
  41 |   callbacks: {
> 42 |     async session({ session, token }) {

Right again, this happens on "next-auth": "^5.0.0-beta.5". After installing the "next-auth": "^5.0.0-beta.9", it disappeared.
I am now fully happy! Right before, I was about to give up. 😅

@ndom91
Copy link
Member

ndom91 commented Feb 10, 2024

Also quick note, Prisma is beginning to release edge compatible clients. In their latest 5.9.1 release they already updated it to not throw when imported in an edge runtime, but only at query-time. So you can at least import it in files imported in middleware, as long as your not making any queries. Should simplify the auth.ts split workaround as well.

Just remember to use @prisma/client@5.9.1+ and session: { strategy: "jwt" } 👍

@idhard
Copy link

idhard commented Feb 11, 2024

i'm facing the same issue. So far the only solution i found is to use Prisma on the edge following this article: https://www.prisma.io/blog/database-access-on-the-edge-8F0t1s1BqOJE.

@ndom91
Copy link
Member

ndom91 commented Feb 13, 2024

Yeah so historically the only option has been Prisma Accelarate. This is just a db connection pooler behind an HTTP endpoint, so you all you needed was an http client (fetch, axios, etc.) and that's something you find in any runtime (edge, node, etc.).

But now they're seemingly slowly but surely shipping "real" edge runtime support.

Upgrading the @primsa/client@5.9.1, using jwt session strategy, and not doing any DB queries in your middleware, for example, will already work today.

@idhard
Copy link

idhard commented Feb 13, 2024

@ndom91 do you know if Prisma edge client is compatible with Supabase Supavisor https://github.com/supabase/supavisor/blob/main/README.md.

@ndom91
Copy link
Member

ndom91 commented Feb 13, 2024

@ndom91 do you know if Prisma edge client is compatible with Supabase Supavisor https://github.com/supabase/supavisor/blob/main/README.md.

No that won't work unfortunately. Prisma "edge client" (i assume you mean the preexisting accelerate thing?) is just an http client sending http requests back n forth to prisma, who on their end pool the connections and respond back with http.

The supabase supavisor seems to be an actual postgres pooler that works over the postgres protocol, not http

@SebastianNarvaez11
Copy link
Author

SebastianNarvaez11 commented Feb 21, 2024

Hello guys, I already implemented the solution proposed by @ConnorC18 and checked the @AmphibianDev repository, I have exactly the same implementation and the same versions of next-auth and prisma, I also separated the functions that use Prisma into independent functions, but now I am having this problem (with Prisma Adapter it doesn't work either)

image

import NextAuth from "next-auth";
import authConfig from "./auth.config";

export const {
  handlers: { GET, POST },
  auth,
  signIn,
  signOut,
} = NextAuth({...authConfig});
import type { NextAuthConfig } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import prisma from "@/lib/prisma";
import bcryptjs from "bcryptjs";
import { LoginSchema } from "./validations";
import { getUserByEmail } from "./actions/users/getUserByEmail";
import { getUserById } from "./actions/users/getUserById";

export default {
  pages: {
    signIn: "/auth/login",
    error: "/auth/error",
  },

  providers: [
    CredentialsProvider({
      async authorize(credentials) {
        const { email, password } = await LoginSchema.validate(credentials);

        //1. Buscar usuario
        const user = await getUserByEmail(email);

        if (!user) return null;
        if (user.deleted || !user.isActive) return null;

        // 2. Comparar contraseñas
        if (!bcryptjs.compareSync(password, user.password)) return null;

        const {
          password: pass,
          createdAt,
          updateAt,
          deleted,
          isActive,
          schoolId,
          ...rest
        } = user;

        return rest;
      },
    }),
  ],

  session: { strategy: "jwt" },

  callbacks: {
    async jwt({ token }) {
      // Aqui puedo modificar la informacion del token
      if (!token.sub) throw Error("Token sin Id");

      //1. Buscar el usuario
      const user = await getUserById(token.sub)

      // 2. Validar estado del usuario
      if (!user) throw Error("No se encontro el usuario del token");
      if (user.deleted || !user.isActive) throw Error("Usuario inactivo");

      const {
        password: pass,
        createdAt,
        updateAt,
        deleted,
        isActive,
        schoolId,
        ...rest
      } = user;

      // 3. Poner informacion necesaria para la session en el token
      token.data = rest;

      return token;
    },

    async session({ session, token }: any) {
      if (token.sub && session.user) {
        session.user = token.data;
      }

      return session;
    },
  },
} satisfies NextAuthConfig;

@renatoastra
Copy link

Hello, any workrounds? im facing this issue too #9836 (comment)

@ndom91
Copy link
Member

ndom91 commented Mar 4, 2024

Hey folks, this issue seems to have gotten a bit off track..

Initially the issue was about session data not being available in middleware, right? And then it shifted towards prisma not working in edge environments. I think we've all gotten to the bottom of the prisma issue, right? If not, here's my current understanding - using prisma@5.9.1+, session: { strategy: 'jwt' } and not doing any database calls with prisma in your middleware or other edge runtime environments should work as of now with the latest versions of everything. Right? I'm pretty sure you no longer have to do the split config thing either since as of prisma@5.9.1 they changed when the errors throws (query time vs. client instantiation).

Regarding the middleware req.auth issue, I've just double checked with the latest next-auth@5.0.0-beta.15 and req.auth is available with both OAuth and Credential providers when setup like how the docs describe:

import { auth } from "./auth"

export default auth((req) => {
  console.log("middleware.auth", !!req.auth)
})

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

If you're having problems that aren't related to the original middleware session issue, please open a new issue.


To the latest commenters, @SebastianNarvaez11 this is an error you're throwing yourself because your getUserById funciton is not findign the user, correct? In the case of credentialsProvider, the token.sub defaults to the profile.id. So make sure yuo're returning at least an id from your authorize fn.

You can enable debug: true in the auth config and see what token.sub you're getting back from the provider, or simply log it in your getUserById. Then you can manually confirm that you have a user in your database with that ID. Is that all good so far?

@renatoastra can you provide more details about your setup and which versions of next-auth and next.js you're using?

Thanks everyone 🙏

@renatoastra
Copy link

Here are the versions of my libraries:

  • prisma: 5.10.2
  • @prisma/client: 5.10.2
  • next: 14.1.0
  • next-auth: 5.0.0-beta.15

While debugging my callbacks and middleware, I discovered the issue. It appears to be related to the prisma query on the JWT callback function, although I'm unsure why. However, placing my Prisma query within a try-catch block resolved the issue!

Here is the code:

    async jwt({ token }) {
      try {
        const query = await getUser(token.sub);
        token.role = query.role;
        token.username = query.username;
        token.avatarUrl = query.avatarUrl;
      } catch (error) {
        console.log("🚀 ~ error:", error);
      }
      return token;
    },

@juanzgc
Copy link

juanzgc commented Mar 10, 2024

When using the auth.ts - authorized middleware function and returning true / false, the authentication works as expected.

However, if I instead use the following: https://github.com/nextauthjs/next-auth/blob/main/apps/examples/nextjs/middleware.ts#L4

Then returning false in the authorized function no longer forces a login. Basically forcing the middleware function to be written in auth.ts, instead of in middleware.ts

@schndra
Copy link

schndra commented Mar 11, 2024

Thanks @ConnorC18

Adding these two lines worked for me :<

  export default {
    providers: [
      .....
    ],
    callbacks: {
      async session({ session, token }) {
       ...
      },
      async jwt({ token }) {
+       if (!token.email) return token;

        const userInDb = await getUserByEmail(token.email);

+      if (!userInDb) return token;

        token.role = userInDb.role;

        return token;
      },
    },
    adapter: PrismaAdapter(prisma),
  } satisfies NextAuthConfig;

@Thotholani
Copy link

I tried the recommended work arounds such as what @ConnorC18 said here but now I am getting a whole different error together. Seems to be related to middleware once more. Any ideas on how to fix this?

⨯ Error [ReferenceError]: Cannot access 'WEBPACK_DEFAULT_EXPORT' before initialization
at (webpack-internal:///(middleware)/./auth.config.ts:3)
at Module.default (webpack-internal:///(middleware)/./auth.config.ts:3:42)
at eval (webpack-internal:///(middleware)/./auth.ts:14:49)
at (middleware)/./auth.ts (file://C:\Users\Thotholani\Desktop\Projects\mini-projects\checkin.next\server\middleware.js:97:1)
at webpack_require (file://C:\Users\Thotholani\Desktop\Projects\mini-projects\checkin.next\server\edge-runtime-webpack.js:37:33)
at fn (file://C:\Users\Thotholani\Desktop\Projects\mini-projects\checkin.next\server\edge-runtime-webpack.js:285:21)
at eval (webpack-internal:///(middleware)/./lib/auth.ts:6:63)
at (middleware)/./lib/auth.ts (file://C:\Users\Thotholani\Desktop\Projects\mini-projects\checkin.next\server\middleware.js:108:1)
at webpack_require (file://C:\Users\Thotholani\Desktop\Projects\mini-projects\checkin.next\server\edge-runtime-webpack.js:37:33)
at fn (file://C:\Users\Thotholani\Desktop\Projects\mini-projects\checkin.next\server\edge-runtime-webpack.js:285:21)
at eval (webpack-internal:///(middleware)/./auth.config.ts:10:67) {
middleware: true

Screenshot 2024-03-30 221544

ConnorC1

@WilliamEklundITHS
Copy link

Hey guys, I found what worked for me was to define the session callback in both auth.ts and auth.config.ts. The Credentials and other callbacks is defined in auth.ts. Now I can retrieve the custom field in the session on both the server and client as well as in the middleware.ts. I am not sure why this works, but here is the code if anyone wants to give it a try (using next-auth-5.0.0-beta.15):
auth.config

import type { NextAuthConfig } from "next-auth";
export default {
  secret: process.env.AUTH_SECRET,
  pages: {
    signIn: "/login", // Define your sign-in page here
  },
  session: {
    strategy: "jwt",
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },

  providers: [],
  callbacks: {
    async session({ token, session }) {
      console.log("session: ", session);
      if (token.sub && session.user) {
        session.user.id = token.sub;
      }
      if (session.user) {
        session.user.name = token.name;
        session.user.email = token.email as string;
        session.user.role = token.role;
      }
      return session;
    },
  },
} satisfies NextAuthConfig;

auth.ts

import Credentials from "next-auth/providers/credentials";
import bcrypt from "bcrypt";
import authConfig from "./auth.config";
import { findUserByEmail, getUserById } from "./data/services/user-service";
export const {
  handlers: { GET, POST },
  auth,
  signIn,
  signOut,
} = NextAuth({
  debug: true,

  ...authConfig,
  providers: [
    Credentials({
      async authorize(credentials) {
        const { email, password } = credentials;
        const existing_user = await findUserByEmail(email as string);
        if (!existing_user) return null;
        const passwordsMatch = await bcrypt.compare(
          password as string,
          existing_user.password
        );
        if (!passwordsMatch) return null;
        const user = {
          profilePicture: existing_user.profilePicture,
          id: existing_user.id,
          name: existing_user.firstName,
          email: existing_user.email,
          role: existing_user.role,
        };
        return user as User;
      },
    }),
  ],

  callbacks: {
    async session({ token, session }) {
      if (token.sub && session.user) {
        session.user.id = token.sub;
      }
      if (session.user) {
        session.user.name = token.name;
        session.user.email = token.email as string;
        session.user.role = token.role;
      }
      return session;
    },
    async jwt({ token, trigger, session, user, account }) {
      try {
        if (!token.sub) return token;
        let existingUser = null;
        if (trigger === "signIn") {
          console.log({ trigger, session, user, account });
          existingUser = await getUserById(token.sub, user.role);
        } else {
          existingUser = await getUserById(token.sub, token.role);
        }
        if (!existingUser) return token;
        token.name = existingUser.firstName;
        token.email = existingUser.email;
        token.role = existingUser.role;
      } catch (error) {
        console.log(error);
      }
      return token;
    },
  },
});

middleware.ts

import NextAuth from "next-auth";
const { auth } = NextAuth(authConfig);

export default auth((req) => {
  console.log(req?.auth?.user);
});

export const config = {
  // Match any URL that does not contain "api/auth", "_next/static", "_next/image" or "favicon.ico" at any position in the URL path.
  matcher: ["/((?!api/auth|_next/static|_next/image|favicon.ico).*)"],
};

@ndom91
Copy link
Member

ndom91 commented Apr 7, 2024

Just want to reiterate, the splitting of your auth config into separate auth.ts and auth.config.ts was only a workaround to support using prisma (or other non-edge compatible adapters). But that isn't necessary in many cases anymore and avoiding it can cut back on a lot of the potential issues folks are running into here.

Example prisma edge setup: https://github.com/ndom91/authjs-prisma-edge-example

@L3on06
Copy link

L3on06 commented May 3, 2024

Big thanks to you @ConnorC18 i looked at your commit at the @AmphibianDev repo how u did it so i fix at mine and it works for me to.

AndrewCK24 added a commit to AndrewCK24/volleybro that referenced this issue May 22, 2024
問題描述:
auth.js callback 中 token, session 的相關處理
應該可以影響到在 req.auth 應該顯示的資訊
但僅能顯示 user 的 name, email, image 三個欄位

解決方式:
將 callback 放在 auth.config.js 中,即可正常顯示完整資訊
nextauthjs/next-auth#9836 (comment)
@AvivHagag
Copy link

@SebastianNarvaez11 @deltasierra96

I also faced this issue, what seems to have worked for me was combining my NextAuth instantiations.

The Fix

You have called NextAuth(...) in src/middleware.ts and src/auth.ts using two different configurations. I was doing a similar thing. What you will want to do is move the content of the NextAuth(...) instantiation in src/auth.ts into src/auth.config.ts so that you have one main config with all your params for NextAuth(...).

Your src/auth.ts should now look a little something like:

import NextAuth from "next-auth";
import authConfig from "@/auth.config";

export const {
  handlers: { GET, POST },
  auth,
  signIn,
  signOut,
} = NextAuth(authConfig);

Your src/auth.config.ts should now have ALL your configuration in it:

import type { NextAuthConfig } from "next-auth";
// ... Prisma Imports

export default {
  providers: [...],
  pages: {
    signIn: "/auth/login",
    error: "/auth/error",
  },
  session: { strategy: "jwt" },

  callbacks: {
    authorized({ request, auth }) {
      console.log("Please Remove Me. This is a POC", auth) // <-- This should have your additional user data!
      return true;
    },
    async session({ token, session }) {
      if (token?.data?.role && session.user) {
        session.user.role = token.data.role;
      }

      return session;
    },
    async jwt({ token }) {
      // ... Your Prisma Code

      token.data = user    

      return token;
    },
  },
} satisfies NextAuthConfig;

This should resolve the issue. I have freehand typed this in GitHub so this code will probably not be perfect and will almost 100% have at least 1 error, but the idea should be there enough to hopefully help the issue.

The (assumed) Root Cause

It seems the actual cause is because the instantiations are different, calling NextAuth(...) in src/middleware.ts and src/auth.ts works perfectly fine after moving all the configuration settings into one file. Thinking logically now (after rage researching all day over not being able to find the solution to this) it makes perfect sense as in the example you provided (very similar to mine) the middleware would have no idea that the session needs manipulating as the session callback is not in the config being used in src/middleware.ts

TL;DR

You need to use one configuration/NextAuth instantiation for both your providers & middleware

  • Make one central auth config
  • Only call NextAuth once and export all the returned objects
  • Import "auth" from the previous step into the middleware
  • You should now see the role in your middleware auth user data.

Just follow the full fix, its not too long...

I hope that fixes your issue - If this is the problem for you, it seems the problem is NOT with next-auth. It may be worth documenting this somewhere though as its (IMO) an easy mistake to make!

its work !! thank you !!!
It took my all day to fix it

@statusunknown418
Copy link

statusunknown418 commented Jul 5, 2024

if anyone is still struggling with this issue (like me for around 2h) remember that after correctly splitting up your config, when the JWT strategy is enabled all the extra properties are accessed through token (not user) on the session callback.

This was the issue for me, decided to share it here (yea a silly mistake)

// src/auth.config.ts
    // ...
    authorized: ({ auth }) => {
      return !!auth?.user.id;
    },
    session: ({ session, user }) => ({
      ...session,
      user: {
        ...session.user,
        id: user.id, // this should be TOKEN.ID
      },
    }),
    // ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Relates to documentation
Projects
None yet
Development

No branches or pull requests