I am currently developing a Next.js application using NextAuth. At the moment, I have implemented login functionality with Google, credentials, and GitHub. However, I encountered an issue where if a user logs in with Google using the email "[email protected]", then logs out and tries to log in with GitHub using an account that shares the same email "[email protected]", they receive an error: OAuthAccountNotLinked.
Within the database model provided by NextAuth, each User has a relation with Accounts which is an array. Therefore, I thought it would be possible to have the same user linked with two different accounts. Am I missing something or is this the default behavior?
Below is my code snippet from [...nextAuth].ts:
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const data = requestWrapper(req, res);
return await NextAuth(...data);
};
export default handler;
// Function to wrap the request parameters
export function requestWrapper(
req: NextApiRequest,
res: NextApiResponse
): [req: NextApiRequest, res: NextApiResponse, opts: NextAuthOptions] {
// Define utility functions
const generateSessionToken = () => randomUUID();
const fromDate = (time: number, date = Date.now()) =>
new Date(date + time * 1000);
const adapter = PrismaAdapter(prisma);
const opts: NextAuthOptions = {
// Include user.id on session
adapter: adapter,
pages: {
signIn: "/login",
},
callbacks: {
session({ session, user }) {
console.log("AAAA");
if (session.user) {
session.user = user;
}
return session;
},
async signIn({ user, account, profile, email, credentials }) {
// Check if this sign in callback is being called in the credentials authentication flow
if (
req.query.nextauth?.includes("callback") &&
req.query.nextauth?.includes("credentials") &&
req.method === "POST"
) {
if (user) {
const sessionToken = generateSessionToken();
const sessionMaxAge = 60 * 60 * 24 * 30;
const sessionExpiry = fromDate(sessionMaxAge);
await adapter.createSession({
sessionToken: sessionToken,
userId: user.id,
expires: sessionExpiry,
});
const cookies = new Cookies(req, res);
cookies.set("next-auth.session-token", sessionToken, {
expires: sessionExpiry,
});
}
}
return true;
},
},
jwt: {
encode: async ({ token, secret, maxAge }) => {
if (
req.query.nextauth?.includes("callback") &&
req.query.nextauth.includes("credentials") &&
req.method === "POST"
) {
const cookies = new Cookies(req, res);
const cookie = cookies.get("next-auth.session-token");
if (cookie) return cookie;
else return "";
}
// Revert to default behaviour when not in the credentials provider callback flow
return encode({ token, secret, maxAge });
},
decode: async ({ token, secret }) => {
if (
req.query.nextauth?.includes("callback") &&
req.query.nextauth.includes("credentials") &&
req.method === "POST"
) {
return null;
}
// Revert to default behaviour when not in the credentials provider callback flow
return decode({ token, secret });
},
},
// Configure authentication providers
secret: process.env.NEXTAUTH_SECRET,
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID as string,
clientSecret: process.env.GITHUB_SECRET as string,
profile(profile, token) {
return {
id: profile.id.toString(),
name: profile.name || profile.login,
image: profile.avatar_url,
email: profile.email,
role: Role.USER,
};
},
}),
GoogleProvider({
clientId: process.env.GOOGLE_ID as string,
clientSecret: process.env.GOOGLE_SECRET as string,
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code",
},
},
}),
CredentialProvider({
name: "CredentialProvider",
credentials: {
email: { label: "Email", type: "text", placeholder: "" },
password: { label: "Password", type: "password" },
},
async authorize(credentials: any, _req): Promise<any | null> {
const userInputs = {
email: credentials.email,
password: credentials.password,
};
const { user } = await loginCredentials(userInputs);
if (user) {
return user;
} else {
return null;
}
},
}),
],
};
return [req, res, opts];
}