2

I am trying to authenticate using next-auth v4 inside nextjs13. My aim is to store the user's email, name, image and Id from the auth provider (Google) into a MongoDB database. However, I keep getting error saying "OAuthAccountNotLinked".

The URL on the browser address bar also changes to "http://localhost:3000/login?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Flogin&error=OAuthAccountNotLinked"

Here are my various files:

app/auth/[...nextauth]/route.js file

import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import User from "../../models/User";
import bcrypt from "bcryptjs";
import dbConnect from "../../lib/dbConnect";
import GoogleProvider from "next-auth/providers/google";
import { MongoDBAdapter } from "@next-auth/mongodb-adapter";
import clientPromise from "../../lib/mongodb";

const handler = NextAuth({
  adapter: MongoDBAdapter(clientPromise),
  session: {
    strategy: "database",
  },
  callbacks: {
    async signIn({ user, account, profile }) {
      console.log("user: ", user);
      console.log("account: ", account);
      console.log(profile);
      return true;
    },
  },
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
    CredentialsProvider({
      name: "credentials",
      async authorize(credentials, req) {
        await dbConnect();

        const { email, password } = credentials;
        if (!email || !password) {
          throw new Error("Email and password required");
        }
        const user = await User.findOne({ email });
        if (!user) {
          throw new Error("Invalid Email or password");
        }
        const isPasswordMatched = await bcrypt.compare(password, user.password);

        if (!isPasswordMatched) {
          throw new Error("Invalid Email or password");
        }

        return user;
      },
    }),
  ],
  pages: {
    signIn: "/auth/login",
  },
  secret: process.env.NEXT_AUTH_SECRET,
});

export { handler as GET, handler as POST };

dbConnect.js

import mongoose from "mongoose";

const MONGODB_URI =
  process.env.SERVER === "dev"
    ? "mongodb://127.0.0.1:27017/mosque-around-me"
    : process.env.MONGODB_URI;

console.log(MONGODB_URI);

if (!MONGODB_URI) {
  throw new Error(
    "Please define the MONGODB_URI environment variable inside .env.local"
  );
}

/**
 * Global is used here to maintain a cached connection across hot reloads
 * in development. This prevents connections growing exponentially
 * during API Route usage.
 */
let cached = global.mongoose;

if (!cached) {
  cached = global.mongoose = { conn: null, promise: null };
}

async function dbConnect() {
  if (cached.conn) {
    return cached.conn;
  }

  if (!cached.promise) {
    const opts = {
      bufferCommands: true,
    };

    cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
      return mongoose;
    });
  }

  try {
    cached.conn = await cached.promise;
  } catch (e) {
    cached.promise = null;
    throw e;
  }

  return cached.conn;
}

export default dbConnect;

mongodb.js (MongoClient promise)

// This approach is taken from https://github.com/vercel/next.js/tree/canary/examples/with-mongodb
import { MongoClient } from "mongodb";

if (!process.env.MONGODB_URI) {
  throw new Error('Invalid/Missing environment variable: "MONGODB_URI"');
}

const uri = process.env.MONGODB_URI;
const options = {};

let client;
let clientPromise;

if (process.env.NODE_ENV === "development") {
  // In development mode, use a global variable so that the value
  // is preserved across module reloads caused by HMR (Hot Module Replacement).
  if (!global._mongoClientPromise) {
    client = new MongoClient(uri, options);
    global._mongoClientPromise = client.connect();
  }
  clientPromise = global._mongoClientPromise;
} else {
  // In production mode, it's best to not use a global variable.
  client = new MongoClient(uri, options);
  clientPromise = client.connect();
}

// Export a module-scoped MongoClient promise. By doing this in a
// separate module, the client can be shared across functions.
export default clientPromise;

user.js (User model schema):

import mongoose from "mongoose";
import bcrypt from "bcryptjs";
// const jwt = require("jsonwebtoken");

const UserSchema = new mongoose.Schema(
  {
    name: {
      type: String,
      minlength: 2,
      maxlength: 50,
      required: [true, "Please provide first name"],
      trim: true,
    },
    phoneNumber: {
      type: String,
    },
    email: {
      type: String,
      match: [
        /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
        "Please provide a valid email",
      ],
      required: [true, "Please provide an email"],
      unique: [true, "Someone is alreay using this email"],
    },
    authProvider: {
      type: String,
      required: [true, "Auth provider required"],
    },
    password: {
      type: String,
      minlength: 8,
      required: [true, "Please provide password"],
    },
    location: {
      type: String,
      minlength: 3,
      trim: true,
      default: "my town",
    },
    lga: {
      type: String,
      minlength: 3,
      trim: true,
      default: "my town",
    },
    state: {
      type: String,
      minlength: 3,
      trim: true,
      default: "my town",
    },
    country: {
      type: String,
      minlength: 3,
      trim: true,
      default: "my town",
    },
    verified: {
      type: Boolean,
      default: false,
    },
    active: {
      type: Boolean,
      default: true,
    },
    verificationCode: {
      type: String,
      length: 4,
    },
    image: String,
    role: {
      type: String,
      required: [true, "Please provide user role"],
      default: "user",
      enum: {
        values: ["staff", "admin", "user"],
        message: "Please select valid role",
      },
    },
  },
  { timestamps: true }
);

// hash the password before saving it
UserSchema.pre("save", async function (next) {
  // exit when login with google and facebook
  if (!this.password) return;

  // exit the function when other fields are updated
  if (!this.isModified("password")) return;

  const salt = await bcrypt.genSalt(10);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

UserSchema.methods.comparePassword = async function (userPassword) {
  const isMatch = await bcrypt.compare(userPassword, this.password);
  return isMatch;
};

// export new User model if not created already
export default mongoose.models.User || mongoose.model("User", UserSchema);

3 Answers 3

1

I've got to solve this issue by adding this to my Providers:

allowDangerousEmailAccountLinking: true

Here is an example:

    providers: [
      GoogleProvider({
        clientId: process.env.GOOGLE_CLIENT_ID,
        clientSecret: process.env.GOOGLE_CLIENT_SECRET,
        allowDangerousEmailAccountLinking: true,

This enables the Adapter to use the linkAccount method. At the end I have in my database one User with two Accounts.

I'm using Next 14.1 and Next-Auth 4.24, with Prisma 5.9 - Postgres DB.

Here you can check the docs. Cheers

0

If you see OAuthAccountNotLinked it means you have already signed in with a different provider that is associated with the same email address.

from my experience with the library and also from other people's comments related to this error, the fact that Next-auth handles each provider as a unique user will always create problems since in most cases we want to bind a Google account with the corresponding Facebook account for example, or with another user from a database.
for our next app these are three different users, this makes sense, but we still want to handle this, so errors such as OAuthAccountNotLinked are always expected.
there is serval similar issues without an optimal solution.

here the error occurred using mongoDB and with only one provider so it can be the same issue as yours.

solution:

I've found out what's causing this. In my case, I'm using a database to store the users, and next-auth uses some tables to do this, accounts and users are two of them. If you have a user in the accounts table, but for some reason, this user isn't in the users table, you'll get this error. To fix this I deleted the this user_id from the accounts and user table and created it again. The column user_id in the accounts table should ALWAYS have the same value as the id of the user in the users table.

if this does not fix the problem, then, it may be, when you are trying to sign in with the "credentials" provider you are already signed in with the "Google auth" provider using the same email address

1
  • I would like to handle this situation and let the user know that the email is already associated with another social account. Which callback sould I use for that ?
    – Simao
    Commented Apr 2 at 21:39
0

The solution is:

If you have same emailId stored in database, and your github is also associated with same email, you will get this error. Try to delete the one in DB, your problem will be solved.

1
  • Welcome to Stack Overflow! Thank you for your answer. Please provide more details about your solution. Code snippets, high quality descriptions, or any relevant information would be great. Clear and concise answers are more helpful and easier to understand for everyone. Edit your answer with specifics to raise the quality of your answer. For more information: How To: Write good answers. Happy coding!
    – AztecCodes
    Commented Nov 1, 2023 at 0:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.