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

Credentials Provider Not Firing Session Callback

Open Suonx002 opened this issue 2 years ago • 24 comments

Description 🐜

Hello,

I'm having trouble with using Credentials Provider where it's not firing the session callback. I've logged the whole process from signin and output with logger functionality.

I'm not sure if it's my [..nextauth] setting or it's a bug within next-auth and prisma adapter.

Can provide the repo link if needed.

Please note that Github Provider are working fine where it generate session token.

Is this a bug in your own project?

Yes

How to reproduce ☕️

import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import GitHubProvider from 'next-auth/providers/github';
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import { PrismaClient } from '@prisma/client';

import { verifyPassword, hashPassword } from '@functionHelpers/auth/passwords';

const prisma = new PrismaClient();

export default NextAuth({
	adapter: PrismaAdapter(prisma),
	secret: process.env.NEXTAUTH_SECRET,
	// session: {
	// 	jwt: true,
	// },
	pages: {
		// signIn: '/auth/signin',
		// signOut: "/auth/logout",
		// error: "/auth/error", // Error code passed in query string as ?error=
	},
	logger: {
		error(code, metadata) {
			console.log({ type: 'inside error logger', code, metadata });
		},
		warn(code) {
			console.log({ type: 'inside warn logger', code });
		},
		debug(code, metadata) {
			console.log({ type: 'inside debug logger', code, metadata });
		},
	},
	providers: [
		GitHubProvider({
			clientId: process.env.GITHUB_CLIENT_ID,
			clientSecret: process.env.GITHUB_CLIENT_SECRET,
		}),
		CredentialsProvider({
			id: 'app-login',
			name: 'App Login',
			credentials: {
				firstName: {
					label: 'First Name',
					type: 'text',
				},
				lastName: {
					label: 'last Name',
					type: 'text',
				},
				email: {
					label: 'Email Address',
					type: 'email',
				},
				password: {
					label: 'Password',
					type: 'password',
				},
			},
			async authorize(credentials, req) {
				console.log('Starting the signup/login process ---');
				try {
					let maybeUser = await prisma.user.findFirst({
						where: {
							email: credentials.email,
						},
					});

					// register process calling
					if (credentials.methodType === 'register') {
						if (!maybeUser) {
							if (
								!credentials.password ||
								!credentials.email ||
								!credentials?.firstName ||
								!credentials.lastName
							) {
								throw new Error('Please fill all of the information');
							}

							maybeUser = await prisma.user.create({
								data: {
									name: `${credentials.firstName} ${credentials.lastName}`,
									firstName: credentials.firstName,
									lastName: credentials.lastName,
									email: credentials.email,
									password: await hashPassword(credentials.password),
								},
							});
						} else {
							throw new Error('User already existed, please login.');
						}
					} else {
						// login process calling
						// user existed, need to verify user credentials

						if (!maybeUser) {
							throw new Error('No user found. Please create one');
						}

						if (!credentials.password || !credentials.email) {
							throw new Error('Please fill out all of the information');
						}

						const isValid = await verifyPassword(
							credentials?.password,
							maybeUser.password
						);
						if (!isValid) {
							throw new Error('Invalid Credentials');
						}
					}

					console.log('Ending the signup/login process ---');

					return maybeUser;
				} catch (error) {
					console.log(error);
					throw error;
				}
			},
		}),
	],
	callbacks: {
		async signIn({ user, account, profile, email, credentials }) {
			console.log('fire signin Callback');
			return true;
		},
		async redirect({ url, baseUrl }) {
			console.log('fire redirect Callback');
			return baseUrl;
		},
		async session({ session, user, token }) {
			console.log('fire SESSION Callback');
			return session;
		},
		async jwt({ token, user, account, profile, isNewUser }) {
			console.log('fire jwt Callback');

			// console.log({ token, user, account, profile, isNewUser });
			return token;
		},
	},
});

Screenshots / Logs 📽


Starting the signup/login process ---
Ending the signup/login process ---
fire signin Callback
fire jwt Callback
fire redirect Callback
{
  type: 'inside debug logger',
  code: 'adapter_getSessionAndUser',
  metadata: {
    args: [
      'eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..MLqO5WWIQ3nyCWSw.9EZyGxkX-5tWPQbfhDCcU_KzEvgt1EDKSEnC_px8RZ6q_Np8EfBYPIMVkBbgDkc7DdBRp3vssSAUWnNNoqjdvWlstqDQZ44roLQR-IUcYaWaOZQzH_Urau6gp6SluL9J2F69_eXnMSCtqT8h6sWzgtfaX4duZ1e6T49ryZr7qZmUHURYGK5Fxgnls4xHjtWEP7VZdZV_aLqIhAD5ZQ2HSmAjOWOgWUl8NoLhrWQoLSoD0g.TmT2wjs_7lhHifh2Er_kUg'   
    ]
  }
}
fire redirect Callback

Environment 🖥

Windows 11

Contributing 🙌🏽

Yes, I am willing to help solve this bug in a PR

Suonx002 avatar Feb 13 '22 23:02 Suonx002

I have the same issue, GitHub settings works correctly fires session callback but not with Credentials settings, here are my settings:

import NextAuth from "next-auth";

import GitHubProvider from "next-auth/providers/github"
import CredentialsProvider from "next-auth/providers/credentials";
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import prisma from '../../../lib/prisma'


export default NextAuth({
    adapter: PrismaAdapter(prisma),
    providers: [
        GitHubProvider({
            clientId: process.env.GITHUB_ID,
            clientSecret: process.env.GITHUB_SECRET
        }),

        CredentialsProvider({
            id: 'app-login',
            name: "Credentials",
            credentials: {
                username: { label: "Username", type: "text", placeholder: "Insert username" },
                password: {  label: "Password", type: "password", placeholder: "Insert password"}
            },
            async authorize(credentials, req) {
                // Add logic here to look up the user from the credentials supplied
                const user = { id: 1, name: 'Admin' };

                if (user) {
                    return user
                } else {
                    return null
                }
            }
        })
    ],
    secret: process.env.SECRET,

    session: {
    },

    jwt: {},
    pages: {},
    callbacks: {
        async signIn(req) {
            return true
        },
        // async redirect(url, baseUrl) { return baseUrl },
        async session(req) {
            return req.session
        },
        // async jwt(token, user, account, profile, isNewUser) { return token }
    },

    events: {},

    theme: {
        colorScheme: "light", // "auto" | "dark" | "light"
        brandColor: "", // Hex color code
        logo: "" // Absolute URL to image
    },

    debug: true,
})

learnercys avatar Feb 20 '22 18:02 learnercys

I had the same problem but with mongodb adapter, and the problem was I didn't pay attention to

So, the missing piece is set the session's strategy to jwt, as follow:

import { MongoDBAdapter } from '@next-auth/mongodb-adapter';
import { verify } from '@node-rs/bcrypt';
import NextAuth, { type User } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import EmailProvider from 'next-auth/providers/email';

import clientPromise from '../../../lib/mongodb';

declare module 'next-auth' {
  interface User {
    password?: string;
  }
}

export default NextAuth({
  adapter: MongoDBAdapter(clientPromise),
  // Configure one or more authentication providers
  providers: [
    EmailProvider({
      name: 'Magic link',
      secret: process.env.NEXTAUTH_SECRET,
      from: process.env.EMAIL_FROM,
      server: process.env.EMAIL_SERVER ?? {
        host: 'localhost',
        port: 1025,
        secure: false,
        ignoreTLS: true,
      },
    }),
    CredentialsProvider({
      name: 'Account',
      credentials: {
        email: {
          label: 'Email',
          type: 'email',
          placeholder: 'Enter your email',
        },
        password: {
          label: 'Password',
          type: 'password',
          placeholder: 'Enter password',
        },
      },
      async authorize(credentials) {
        if (!credentials) return null;

        const client = await getMongoClient();
        const users = client.db().collection<User>('users');
        const user = await users.findOne({ email: credentials.email });

        if (!user) return null;

        if (
          typeof user.password === 'string' &&
          !(await verify(credentials.password, user.password))
        ) {
          return null;
        } else {
          delete user.password;
        }

        return user;
      },
    }),
    // ...add more providers here
  ],
  debug: process.env.NODE_ENV === 'development',
  session: {
    // Set to jwt in order to CredentialsProvider works properly
    strategy: 'jwt'
  }
});

Edit:
Link to the docs https://next-auth.js.org/configuration/providers/credentials

leosuncin avatar Feb 20 '22 23:02 leosuncin

I had the same problem but with mongodb adapter, and the problem was I didn't pay attention to

So, the missing piece is set the session's strategy to jwt, as follow:

import { MongoDBAdapter } from '@next-auth/mongodb-adapter';
import { verify } from '@node-rs/bcrypt';
import NextAuth, { type User } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import EmailProvider from 'next-auth/providers/email';

import clientPromise from '../../../lib/mongodb';

declare module 'next-auth' {
  interface User {
    password?: string;
  }
}

export default NextAuth({
  adapter: MongoDBAdapter(clientPromise),
  // Configure one or more authentication providers
  providers: [
    EmailProvider({
      name: 'Magic link',
      secret: process.env.NEXTAUTH_SECRET,
      from: process.env.EMAIL_FROM,
      server: process.env.EMAIL_SERVER ?? {
        host: 'localhost',
        port: 1025,
        secure: false,
        ignoreTLS: true,
      },
    }),
    CredentialsProvider({
      name: 'Account',
      credentials: {
        email: {
          label: 'Email',
          type: 'email',
          placeholder: 'Enter your email',
        },
        password: {
          label: 'Password',
          type: 'password',
          placeholder: 'Enter password',
        },
      },
      async authorize(credentials) {
        if (!credentials) return null;

        const client = await getMongoClient();
        const users = client.db().collection<User>('users');
        const user = await users.findOne({ email: credentials.email });

        if (!user) return null;

        if (
          typeof user.password === 'string' &&
          !(await verify(credentials.password, user.password))
        ) {
          return null;
        } else {
          delete user.password;
        }

        return user;
      },
    }),
    // ...add more providers here
  ],
  debug: process.env.NODE_ENV === 'development',
  session: {
    // Set to jwt in order to CredentialsProvider works properly
    strategy: 'jwt'
  }
});

This is awesome, thank you for your help!

Suonx002 avatar Feb 21 '22 01:02 Suonx002

  session: {
    // Set to jwt in order to CredentialsProvider works properly
    strategy: 'jwt'
  }

You are AMAZING! I have been scratching my head for a week and yet, it's so simple... Thanks so much man!

larsniet avatar Feb 22 '22 22:02 larsniet

session: {
// Set to jwt in order to CredentialsProvider works properly
 strategy: 'jwt'
 }

Thank you so much it works perfectly!!!

markmendozadev avatar Mar 31 '22 10:03 markmendozadev

@leosuncin where did you see that notice? I'm not seeing that on the current documentation

pianomansam avatar Apr 06 '22 14:04 pianomansam

@leosuncin perhaps we should display it in both places, then, because I only knew about the documentation page I linked to

pianomansam avatar Apr 06 '22 15:04 pianomansam

Weirdly this issue didn't affect me until I started using multiple providers - when I only had credential provider it worked without any issues. Definitely not obvious in the current docs.

OffBy0x01 avatar May 24 '22 14:05 OffBy0x01

session: { // Set to jwt in order to CredentialsProvider works properly strategy: 'jwt' }

Thanks a lot!!

vedantnn71 avatar Aug 19 '22 08:08 vedantnn71

You can also use the 'database' strategy with this work-around. https://github.com/nextauthjs/next-auth/discussions/4394#discussioncomment-3293618

Handfish avatar Aug 24 '22 17:08 Handfish

I had a way to use jwt strategy to work with session by default in latest next-auth

https://github.com/nextauthjs/next-auth/discussions/4394#discussioncomment-3293618

pencilcheck avatar Mar 20 '23 12:03 pencilcheck

I had the same problem but with mongodb adapter, and the problem was I didn't pay attention to

So, the missing piece is set the session's strategy to jwt, as follow:

import { MongoDBAdapter } from '@next-auth/mongodb-adapter';
import { verify } from '@node-rs/bcrypt';
import NextAuth, { type User } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import EmailProvider from 'next-auth/providers/email';

import clientPromise from '../../../lib/mongodb';

declare module 'next-auth' {
  interface User {
    password?: string;
  }
}

export default NextAuth({
  adapter: MongoDBAdapter(clientPromise),
  // Configure one or more authentication providers
  providers: [
    EmailProvider({
      name: 'Magic link',
      secret: process.env.NEXTAUTH_SECRET,
      from: process.env.EMAIL_FROM,
      server: process.env.EMAIL_SERVER ?? {
        host: 'localhost',
        port: 1025,
        secure: false,
        ignoreTLS: true,
      },
    }),
    CredentialsProvider({
      name: 'Account',
      credentials: {
        email: {
          label: 'Email',
          type: 'email',
          placeholder: 'Enter your email',
        },
        password: {
          label: 'Password',
          type: 'password',
          placeholder: 'Enter password',
        },
      },
      async authorize(credentials) {
        if (!credentials) return null;

        const client = await getMongoClient();
        const users = client.db().collection<User>('users');
        const user = await users.findOne({ email: credentials.email });

        if (!user) return null;

        if (
          typeof user.password === 'string' &&
          !(await verify(credentials.password, user.password))
        ) {
          return null;
        } else {
          delete user.password;
        }

        return user;
      },
    }),
    // ...add more providers here
  ],
  debug: process.env.NODE_ENV === 'development',
  session: {
    // Set to jwt in order to CredentialsProvider works properly
    strategy: 'jwt'
  }
});

Edit: Link to the docs https://next-auth.js.org/configuration/providers/credentials

Send by God !

ElvisDev187 avatar May 15 '23 07:05 ElvisDev187

Why it's no the default config ?

ElvisDev187 avatar May 15 '23 07:05 ElvisDev187

@ElvisDev187 this is from the documentation

Choose how you want to save the user session. The default is "jwt", an encrypted JWT (JWE) in the session cookie.

If you use an adapter however, we default it to "database" instead. You can still force a JWT session by explicitly defining "jwt".

When using "database", the session cookie will only contain a sessionToken value, which is used to look up the session in the database.

mariosantosdev avatar May 17 '23 22:05 mariosantosdev

Ok now it's fine for me

Le mer. 17 mai 2023 à 23:35, Mário Santos @.***> a écrit :

@ElvisDev187 https://github.com/ElvisDev187 this is from the documentation

Choose how you want to save the user session. The default is "jwt", an encrypted JWT (JWE) in the session cookie.

If you use an adapter however, we default it to "database" instead. You can still force a JWT session by explicitly defining "jwt".

When using "database", the session cookie will only contain a sessionToken value, which is used to look up the session in the database.

— Reply to this email directly, view it on GitHub https://github.com/nextauthjs/next-auth/issues/3970#issuecomment-1552171983, or unsubscribe https://github.com/notifications/unsubscribe-auth/AUBZVKPZTHN3W3SADBSWOGLXGVHBLANCNFSM5OJ4SMSA . You are receiving this because you were mentioned.Message ID: @.***>

ElvisDev187 avatar May 19 '23 06:05 ElvisDev187

@mariosantosdev thx, I understand better now, I was a bit confused at first.

ElvisDev187 avatar May 19 '23 06:05 ElvisDev187

  session: {
    // Set to jwt in order to CredentialsProvider works properly
    strategy: 'jwt'
  }

You are AMAZING! I have been scratching my head for a week and yet, it's so simple... Thanks so much man!

JBPORTALS avatar Jun 08 '23 06:06 JBPORTALS

I had the same problem. I use TiDB and GoogleProvider. I add relationMode = "prisma". It works!

schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
+  relationMode = "prisma"
}

https://github.com/planetscale/discussion/discussions/18

Rasukarusan avatar Sep 13 '23 14:09 Rasukarusan

Hi everyone. I searched online and read through all posts in this thread, but I cannot seem to get my set up to fire the "session" callback. I was extremely excited to see that I missed the "session: { strategy: 'jwt' }" as other did, but, alas, after adding it to my NextAuth options, the "session" callback is still not firing.

I have trimmed down everything to make sure only "good" data is getting sent through the system, creating a simple hard-coded user to return from the "authorize" function. After signing in, "authorize" runs as it should, the "jwt" callback is fired, but I never see the console log printed out in the "session" callback. Any help you all could provide would be much appreciated.

I'm simply using the default "Sign In" and "Log Out" pages and just the "Credentials Provider" with no adapter. Here's my code for the "/api/auth/[...nextauth]/route.ts" file:

import NextAuth from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"

export const authOptions: NextAuthOptions = {
	providers: [
		CredentialsProvider({
			name: "mSecure Credentials",
			credentials: {
				username: {
					label: "Username",
					type: "text",
					placeholder: "username"
				},
				password: {
					label: "Password",
					type: "password",
					placeholder: "enter password"
				}
			},
			async authorize(credentials) {
				const user = {
					id: "1000",
					email: "[email protected]",
					name: "Mike Reilley"
				}

				if (user) {
					return user
				}
				return null
			}
		})
	],
	callbacks: {
		async jwt({ token, user, session }) {
			console.log("jwt callback fired")
			return token
		},
		async session({ session, token, user }) {
			console.log("session callback fired")
			return session
		}
	},
	debug: process.env.NODE_ENV === "development",
	session: {
		strategy: "jwt",
		// Seconds - How long until an idle session expires and is no longer valid.
		maxAge: 30 * 24 * 60 * 60 // 30 days
	}
}

const handler = NextAuth(authOptions)

export { handler as GET, handler as POST }

deelydian avatar Nov 10 '23 17:11 deelydian

@deelydian - at a glance the only obvious difference between what you have here and what I got working is the lack of secret: process.env.NEXT_AUTH_SECRET within options - not a required in NODE_ENV === 'development' but is required in NODE_ENV === 'production'.

Haven't checked but I'd expect session callback is only called when you call useSession in a page.

OffBy0x01 avatar Nov 10 '23 21:11 OffBy0x01

Thank you for the response @OffBy0x01. I do have the process.env.NEXT_AUTH_SECRET, so that definitely wasn't the problem. So far as I can tell, everything is actually working. I was confused because everything I read and watched was showing the "session" callback getting called every time an update was made to the code in VS Code and the browser page was refreshed either manually or through live updating. That had to be because there was a "useSession" call being made, but I just didn't see it.

At any rate, I can confirm everything is working correctly. I'm able to use "useSession" in client components and "getToken" in server components to get the information I need to authenticate with my Parse backend. Thank you again for your response.

deelydian avatar Nov 13 '23 23:11 deelydian

Did you find any solution. I'm currently in a similar situation. I have the NEXT_AUTH_SECRET set up. I also did hardcode a user but it turns out when i try to sign in it doesn't fire the authenticate. I get redirected to http://localhost:3000/api/auth/error. and my browser console logs: GET http://localhost:3000/api/auth/providers 404 (Not Found) [next-auth][error][CLIENT_FETCH_ERROR] https://next-auth.js.org/errors#client_fetch_error Unexpected token '<', "<!DOCTYPE "... is not valid JSON {error: {…}, url: '/api/auth/providers', message: Unexpected token '<', "<!DOCTYPE "... is not valid JSON}

Current on; "next": "^14.0.3", "next-auth": "^4.24.5",

here i how my code looks currently

src\app\api\auth\[...nextauth]\route.ts

import {
    getServerSession, type NextAuthOptions,
} from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { authService } from "./userAuthentication";

export const authOptions: NextAuthOptions = {
    secret: process.env.NEXTAUTH_SECRET,
    debug: process.env.NODE_ENV === "development",
    session: {
        strategy: "jwt",
    },
    pages: {
        signIn: '/user/login',
        error: '/user/login',
    },
    providers: [
        Credentials({
            name: "Credentials",
            type: "credentials",
            credentials: {
                username: { label: "Username", type: "text" },
                password: { label: "Password", type: "password" }
            },
            async authorize(credentials) {
                console.log("credentials reached");
                const { username, password } = credentials as {
                    username: string
                    password: string
                };
                try {
                    const user = await authService.authenticate(username, password);
                    if (user) return user;
                } catch (error) {
                    console.log(error)
                }
            }
        })
    ],
    callbacks: {
        async jwt({ token, account, profile }) {
            if (account && account.type === "credentials") {
                token.userId = account.providerAccountId;
            }
            return token;
        },
        async session({ session, token, user }) {
            session.user.id = token.userId;
            return session;
        },
        async redirect({ url, baseUrl }) {
            return "/";
        },
    },
};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };



../../userAuthenticate.ts

function authenticate(username: string, password: string) {
  const user = {
    id: "3",
    name: "Test User",
    email: "[email protected]"
  };
  return Promise.resolve(user);
}

export const authService = {
  authenticate,
};

SakoeCourage avatar Jan 25 '24 23:01 SakoeCourage

@SakoeCourage We're having this issue with the signIn() function of next-auth/react as well. Logging in works, but I have to do something like signIn().catch(router.refresh).

@balazsorban44 We (https://github.com/teamhanko) are building a NextAuth provider for our open-source passkeys API, so .catching everything wouldn't be feasible :\

We have to call signIn() from our lib because we need to pass things into it that have to be computed on the client. We can't easily get access to the signIn() function exported from auth.ts of the user, which I'm guessing would solve this issue too instead of using next-auth/react — is there a recommended way of doing this?

Do you know if this issue is related to the original post or if it's a separate thing? For what it's worth, I initially landed here because I searched for why signIn() didn't refresh my current route/work until I refreshed the page, hah

merlindru avatar May 08 '24 09:05 merlindru