You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js13集成next-auth v4时遭遇OAuthAccountNotLinked错误求助

问题描述

我在Next.js 13中使用next-auth v4实现认证功能,目标是将Google认证提供商返回的用户邮箱、姓名、头像及ID存储到MongoDB数据库中,但始终遇到OAuthAccountNotLinked错误,浏览器地址栏会跳转到:http://localhost:3000/login?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Flogin&error=OAuthAccountNotLinked


相关文件代码

app/auth/[...nextauth]/route.js 文件

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

问题分析与解决方案

核心问题原因

  1. 双数据存储冲突:同时使用MongoDBAdapter(原生MongoDB驱动)和自定义Mongoose User模型,两者的用户数据结构不兼容。next-auth适配器自动创建的用户没有authProvider和password必填字段,导致OAuth用户创建失败。
  2. 账号关联逻辑缺失:当邮箱已通过Credentials方式注册时,next-auth无法将其与新的Google OAuth账号关联,触发OAuthAccountNotLinked错误。

解决方案一:统一使用MongoDB Adapter,适配自定义模型

1. 修改User Schema,移除强约束

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,
      default: "credentials", // 设置默认值,移除必填
    },
    password: {
      type: String,
      minlength: 8,
      required: false, // OAuth用户不需要密码,移除必填
    },
    // 其余字段保持不变
  },
  { timestamps: true }
);

2. 完善signIn回调,处理用户创建与关联

callbacks: {
  async signIn({ user, account, profile }) {
    await dbConnect();
    const existingUser = await User.findOne({ email: user.email });

    if (!existingUser) {
      // 创建OAuth用户
      await User.create({
        name: user.name,
        email: user.email,
        image: user.image,
        authProvider: account.provider,
      });
      return true;
    }

    // 关联已有用户与当前OAuth提供商
    if (!existingUser.authProvider.includes(account.provider)) {
      existingUser.authProvider = `${existingUser.authProvider},${account.provider}`;
      await existingUser.save();
    }
    return true;
  },
},

3. 调整CredentialsProvider的查询逻辑

// 在authorize方法中,只查询credentials类型的用户
const user = await User.findOne({ email, authProvider: "credentials" });

解决方案二:放弃MongoDB Adapter,完全自定义认证流程

1. 移除Adapter配置,改用JWT会话策略

const handler = NextAuth({
  session: {
    strategy: "jwt",
  },
  callbacks: {
    async signIn({ user, account, profile }) {
      await dbConnect();
      const existingUser = await User.findOne({ email: user.email });

      if (!existingUser) {
        // 创建OAuth用户
        await User.create({
          name: user.name,
          email: user.email,
          image: user.image,
          authProvider: account.provider,
          password: "",
        });
        return true;
      }

      // 检查是否已关联当前提供商
      if (existingUser.authProvider === account.provider) {
        return true;
      }

      // 未关联则抛出错误,可自定义提示
      throw new Error("该邮箱已通过其他方式注册,请使用对应方式登录或关联账号");
    },
    async jwt({ token, user }) {
      // 将用户信息存入JWT
      if (user) {
        token.id = user._id;
        token.image = user.image;
        token.authProvider = user.authProvider;
      }
      return token;
    },
    async session({ session, token }) {
      // 将JWT信息同步到会话
      session.user.id = token.id;
      session.user.image = token.image;
      session.user.authProvider = token.authProvider;
      return session;
    },
  },
  // 其余配置保持不变
});

额外注意事项

  • 清理MongoDB中重复用户数据:如果同一邮箱存在于next-auth自动创建的users集合和自定义users集合,需手动合并或删除重复项。
  • 验证Google OAuth回调地址:在Google Cloud控制台中,回调地址必须设置为http://localhost:3000/api/auth/callback/google。
  • 确认环境变量配置:NEXT_AUTH_SECRET、GOOGLE_CLIENT_ID、GOOGLE_CLIENT_SECRET和MONGODB_URI需正确配置。

内容的提问来源于stack exchange,提问作者Bello Shehu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 19:24:53