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 bcryptpare(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 .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.methodsparePassword = async function (userPassword) {
const isMatch = await bcryptpare(userPassword, this.password);
return isMatch;
};
// export new User model if not created already
export default mongoose.models.User || mongoose.model("User", UserSchema);
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.pare(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./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.parePassword = async function (userPassword) {
const isMatch = await bcrypt.pare(userPassword, this.password);
return isMatch;
};
// export new User model if not created already
export default mongoose.models.User || mongoose.model("User", UserSchema);
Share
Improve this question
asked Jun 26, 2023 at 7:59
Bello ShehuBello Shehu
3454 silver badges8 bronze badges
3 Answers
Reset to default 3I'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
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 ments 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
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.