Rayy
Rayy8mo ago

Unable to authenticate Convex with next auth when provider is Credentials.

I have followed this post to setup the adapter https://stack.convex.dev/nextauth-adapter, and authenticate Convex with next auth, and it works with providers with github and google, but it does not with credentials. I am able to authenticate in the client side still with credentials, but unable to authenticate the same with Convex. It is not returning any sessions with credentials provider. Here is my auth.ts
export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth({
pages: {
signIn: "/auth/login",
error: "/auth/error",
},
events: {
async linkAccount({ user }) {
await updateUserEmailVerification(user.id as Id<"users">);
},
},
callbacks: {
async signIn({ user, account }) {
if (account?.provider !== "credentials") return true;

if (!user.email) return false;

const existingUser = await getUserByEmail(user.email);

if (!existingUser?.emailVerified) return false;

return true;
},
async session({ session }) {
console.log(session);
const privateKey = await importPKCS8(
process.env.CONVEX_AUTH_PRIVATE_KEY!,
"RS256"
);
const convexToken = await new SignJWT({
sub: session.userId,
})
.setProtectedHeader({ alg: "RS256" })
.setIssuedAt()
.setIssuer(CONVEX_SITE_URL)
.setAudience("convex")
.setExpirationTime("1h")
.sign(privateKey);
return { ...session, convexToken };
},
},
adapter: ConvexAdapter,
...authConfig,
});

declare module "next-auth" {
interface Session {
convexToken: string;
}
}
export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth({
pages: {
signIn: "/auth/login",
error: "/auth/error",
},
events: {
async linkAccount({ user }) {
await updateUserEmailVerification(user.id as Id<"users">);
},
},
callbacks: {
async signIn({ user, account }) {
if (account?.provider !== "credentials") return true;

if (!user.email) return false;

const existingUser = await getUserByEmail(user.email);

if (!existingUser?.emailVerified) return false;

return true;
},
async session({ session }) {
console.log(session);
const privateKey = await importPKCS8(
process.env.CONVEX_AUTH_PRIVATE_KEY!,
"RS256"
);
const convexToken = await new SignJWT({
sub: session.userId,
})
.setProtectedHeader({ alg: "RS256" })
.setIssuedAt()
.setIssuer(CONVEX_SITE_URL)
.setAudience("convex")
.setExpirationTime("1h")
.sign(privateKey);
return { ...session, convexToken };
},
},
adapter: ConvexAdapter,
...authConfig,
});

declare module "next-auth" {
interface Session {
convexToken: string;
}
}
I feel like I need to access the token callback to pass theJWT. Have I missed something in the docs?
15 Replies
Michal Srb
Michal Srb8mo ago
What do your providers look like?
Rayy
RayyOP8mo ago
export default {
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
Github({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
}),
Credentials({
async authorize(credentials) {
const validatedFields = loginSchema.safeParse(credentials);

if (validatedFields.success) {
const { email, password } = validatedFields.data;

const user = await getUserByEmail(email);

if (!user || !user.password) return null;

const passwordsMatch = await bcrypt.compare(password, user.password);

if (passwordsMatch) return user;
}
return null;
},
}),
],
} satisfies NextAuthConfig;
export default {
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
Github({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
}),
Credentials({
async authorize(credentials) {
const validatedFields = loginSchema.safeParse(credentials);

if (validatedFields.success) {
const { email, password } = validatedFields.data;

const user = await getUserByEmail(email);

if (!user || !user.password) return null;

const passwordsMatch = await bcrypt.compare(password, user.password);

if (passwordsMatch) return user;
}
return null;
},
}),
],
} satisfies NextAuthConfig;
Here you go.
Michal Srb
Michal Srb8mo ago
Doesn't look like this ever creates a user given credentials?
Michal Srb
Michal Srb8mo ago
I would move the password hashing and checking logic to Convex, here's an example: https://github.com/xixixao/convex-auth/blob/main/convex/auth.ts
GitHub
convex-auth/convex/auth.ts at main · xixixao/convex-auth
Demonstration of authentication purely via Convex. Contribute to xixixao/convex-auth development by creating an account on GitHub.
Rayy
RayyOP8mo ago
It does, i guess. getUserByEmail is actually a wrapper of the query being fetched.
export const getUserByEmail = (email: string) => {
return fetchQuery(api.users.getUserByEmail, {
email,
});
};
export const getUserByEmail = (email: string) => {
return fetchQuery(api.users.getUserByEmail, {
email,
});
};
I have a register form which calls the register action on submit.
export const register = async (values: RegisterSchema) => {
const validatedFields = registerSchema.safeParse(values);

if (!validatedFields.success) {
return { error: "Invdalid fields" };
}

const { email, password } = validatedFields.data;
const hashedPassword = await bcrypt.hash(password, 10);

try {
const existingUser = await getUserByEmail(email);

if (existingUser && existingUser.emailVerified) {
return { error: "You already have an account with us, please login." };
}

if (existingUser && !existingUser.emailVerified) {
await deleteUnverifiedUser(existingUser._id);
}

await createUser(email, hashedPassword);

const verificationCode = await generateTwoFactorCode(email);

const res = await sendVerificationEmail(
verificationCode.email,
verificationCode.code
);

if (res.error) {
return { error: "Something went wrong!" };
}

return { success: "A code has been sent to your email" };
} catch (error) {
throw error;
}
};
export const register = async (values: RegisterSchema) => {
const validatedFields = registerSchema.safeParse(values);

if (!validatedFields.success) {
return { error: "Invdalid fields" };
}

const { email, password } = validatedFields.data;
const hashedPassword = await bcrypt.hash(password, 10);

try {
const existingUser = await getUserByEmail(email);

if (existingUser && existingUser.emailVerified) {
return { error: "You already have an account with us, please login." };
}

if (existingUser && !existingUser.emailVerified) {
await deleteUnverifiedUser(existingUser._id);
}

await createUser(email, hashedPassword);

const verificationCode = await generateTwoFactorCode(email);

const res = await sendVerificationEmail(
verificationCode.email,
verificationCode.code
);

if (res.error) {
return { error: "Something went wrong!" };
}

return { success: "A code has been sent to your email" };
} catch (error) {
throw error;
}
};
Each function is actually a wrapper of the query or mutation being called from convex. My applicaiton registers the user just like it should, and I can see the data in the convex database, and I am able to login with the same too without any issues. I just am not able to authenticate it with Convex, so that I can authenticate my Convex functions.
Michal Srb
Michal Srb8mo ago
Does the logic in signIn callback work? What does the console.log(session); line print?
Rayy
RayyOP8mo ago
The signIn callback, logs the user and account
USER {
_creationTime: 1716284082776.5615,
_id: 'j572z018e0g9t800511wpmwzgh6sh56z',
email: '...',
emailVerified: 1716284125046,
password: '$2a$10$/vA1Y1bDolexsMk0KJErR.o0tDDfOWKZjsnQIDU49vq6YT8CGpLy2',
id: '53fea5c8-1574-4394-a812-ff86f944526e'
}
ACCOUNT {
providerAccountId: '53fea5c8-1574-4394-a812-ff86f944526e',
type: 'credentials',
provider: 'credentials'
}
USER {
_creationTime: 1716284082776.5615,
_id: 'j572z018e0g9t800511wpmwzgh6sh56z',
email: '...',
emailVerified: 1716284125046,
password: '$2a$10$/vA1Y1bDolexsMk0KJErR.o0tDDfOWKZjsnQIDU49vq6YT8CGpLy2',
id: '53fea5c8-1574-4394-a812-ff86f944526e'
}
ACCOUNT {
providerAccountId: '53fea5c8-1574-4394-a812-ff86f944526e',
type: 'credentials',
provider: 'credentials'
}
But the sessions callback logs null. But it works when I set the session strategy as jwt, and in the jwt callback return the token. I feel like there is something I am misunderstanding here.
Michal Srb
Michal Srb8mo ago
Have you tried add debug: true to NextAuth and see if it prints anything useful?
Rayy
RayyOP8mo ago
This is what I get after adding debug as true
[auth][debug]: adapter_getSessionAndUser {
"args": [
"eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwia2lkIjoiSDlkdS1xaE16RXBaUjBhSUhIYVpXUnN0bnBZcnVwY2V2SFN5RHhOc3pfeEgtV0RnNHJPZ2o4M1UzSTRlWU5fYjVHLXdNVHVOOU1zNWtLZFprRGJuV2cifQ..fAWh7EV-iq1MMjSEhPYGzg.qN_eqRC-aycxiHqX1DZ8fY-PAPuV6bXyu7UEKBVDzk1g6sv6OF_SZon6OWZN5pEM5IhKdQByTrx4LmUrWnfv50XHdY1QShL6yvVKjb8s5irWFaPQ_8AMz5kKJqqslIbnm-yn3VyCbBuCxnNsgO0C4Bb85Jv_KcYSGDOdD7w6TVc8I24DStk8bnDgx5_TAc7rZGygH6VF-tEpNNkb5-QapQ.6CK7XCb-jAXCkBCk5SVUFgAZrnZkB_y80xmg4BaZg-Q"
]
}
[auth][debug]: adapter_getSessionAndUser {
"args": [
"eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwia2lkIjoiSDlkdS1xaE16RXBaUjBhSUhIYVpXUnN0bnBZcnVwY2V2SFN5RHhOc3pfeEgtV0RnNHJPZ2o4M1UzSTRlWU5fYjVHLXdNVHVOOU1zNWtLZFprRGJuV2cifQ..fAWh7EV-iq1MMjSEhPYGzg.qN_eqRC-aycxiHqX1DZ8fY-PAPuV6bXyu7UEKBVDzk1g6sv6OF_SZon6OWZN5pEM5IhKdQByTrx4LmUrWnfv50XHdY1QShL6yvVKjb8s5irWFaPQ_8AMz5kKJqqslIbnm-yn3VyCbBuCxnNsgO0C4Bb85Jv_KcYSGDOdD7w6TVc8I24DStk8bnDgx5_TAc7rZGygH6VF-tEpNNkb5-QapQ.6CK7XCb-jAXCkBCk5SVUFgAZrnZkB_y80xmg4BaZg-Q"
]
}
Michal Srb
Michal Srb8mo ago
Hmm, I'm honestly not sure. I'd have to try credentials and see if it works. You could ask on the Auth.js discord/github too.
Rayy
RayyOP8mo ago
Will be doing that. Thanks for the time tho.
Rayy
RayyOP8mo ago
Hey @Michal Srb , This is the thread I came across https://github.com/nextauthjs/next-auth/issues/3970 So apparently the credentials provider does not fire the session callback.
GitHub
Credentials Provider Not Firing Session Callback · Issue #3970 · n...
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 func...
Michal Srb
Michal Srb8mo ago
Ugh that's uber frustrating You can use the JWT strategy with Convex if you need to though.
Rayy
RayyOP8mo ago
How should I implement that to my existing code? Can you help me with it.
Michal Srb
Michal Srb8mo ago
You shouldn't need many changes. You just won't get sessions stored in the Convex DB. Here's a branch of the example repo that uses the JWT strategy: https://github.com/get-convex/convex-nextauth-template/blob/github-no-db/auth.ts
GitHub
convex-nextauth-template/auth.ts at github-no-db · get-convex/conve...
Example of using Auth.js (NextAuth) with Convex. Contribute to get-convex/convex-nextauth-template development by creating an account on GitHub.

Did you find this page helpful?