skip to Main Content

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);

2

Answers


  1. 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

    Login or Signup to reply.
  2. The solution is:

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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search