Search code examples
next.jsnext-authdrizzle

Next-auth does not detect linked account after first sign in (OAuthAccountNotLinked)


I am on Next.js 14 wth next-auth 4, libSQL, and drizzle orm. Signing in only works once (when account & user are not in database), and subsequent sign-ins return OAuthAccountNotLinked error even though I only use GoogleProvider.

app/api/auth/[...nextauth]/options.ts

import type { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { db } from "@/lib/db";

export const options: NextAuthOptions = {
  adapter: DrizzleAdapter(db),
  secret: process.env.NEXTAUTH_SECRET,
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID as string,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
      authorization: {
        params: {
          prompt: "consent",
          access_type: "offline",
          response_type: "code",
        },
      },
    }),
  ],
  session: {
    strategy: "jwt",
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },
  pages: {
    signIn: "/signin",
    error: "/signin",
  },
};

schema.ts

import {
  integer,
  sqliteTable,
  text,
  primaryKey,
} from "drizzle-orm/sqlite-core";
import type { AdapterAccount } from "@auth/core/adapters";

export const users = sqliteTable("user", {
  id: text("id").notNull().primaryKey(),
  name: text("name"),
  email: text("email").notNull(),
  emailVerified: integer("emailVerified", { mode: "timestamp_ms" }),
  image: text("image"),
});

export const accounts = sqliteTable(
  "account",
  {
    userId: text("userId")
      .notNull()
      .references(() => users.id, { onDelete: "cascade" }),
    type: text("type").$type<AdapterAccount["type"]>().notNull(),
    provider: text("provider").notNull(),
    providerAccountId: text("providerAccountId").notNull(),
    refresh_token: text("refresh_token"),
    access_token: text("access_token"),
    expires_at: integer("expires_at"),
    token_type: text("token_type"),
    scope: text("scope"),
    id_token: text("id_token"),
    session_state: text("session_state"),
  },
  (account) => ({
    compoundKey: primaryKey({
      columns: [account.provider, account.providerAccountId],
    }),
  })
);

export const verificationTokens = sqliteTable(
  "verificationToken",
  {
    identifier: text("identifier").notNull(),
    token: text("token").notNull(),
    expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
  },
  (vt) => ({
    compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
  })
);

I set allowDangerousLinking to true in GoogleProvider options as a workaround, and it does work, but I get this error in the console:

LibsqlError: SQLITE_CONSTRAINT: SQLite error: UNIQUE constraint failed: account.provider, account.providerAccountId

This leads me to believe it's not detecting the linked account and trying to insert another one, even though it already exists.


Solution

  • This fixed it for me: https://github.com/nextauthjs/next-auth/issues/8377#issuecomment-1704299629

    Override the getUserByAccount method from DrizzleAdapter and make it async to await the results

    function getAdapter(): Adapter {
      return {
        ...DrizzleAdapter(db),
        async getUserByAccount(providerAccountId) {
          const results = await db
            .select()
            .from(accounts)
            .leftJoin(users, eq(users.id, accounts.userId))
            .where(
              and(
                eq(accounts.provider, providerAccountId.provider),
                eq(accounts.providerAccountId, providerAccountId.providerAccountId),
              ),
            )
            .get();
    
          return results?.user ?? null;
        },
      };
    }
    
    ...
    NextAuth({
      adapter: getAdapter(),
    });