# Fixing OAuthAccountNotLinked Error in NextAuth.js

> A straightforward guide to enabling multi-provider authentication in Next.js 15 with NextAuth.js.

Source: https://edesy.in/blog/multi-provider-authentication-nextauth

## The Problem

We wanted users to sign in with multiple OAuth providers (Keycloak, Google, GitHub) using the same email address. By default, NextAuth.js blocks this with an error:

```
This email is already associated with another account.
Please sign in with the provider you used previously.
```

The error code is `OAuthAccountNotLinked`.

## Why NextAuth Blocks This

It's a security feature. If someone creates an unverified account with your email on a provider that doesn't verify emails, they could potentially access your account.

## The Solution

NextAuth.js has an option called `allowDangerousEmailAccountLinking` that allows this behavior when you trust your OAuth providers.

## What Didn't Work

### Attempt 1: Global Configuration

```typescript
export const authOptions: NextAuthOptions = {
  adapter: PrismaAdapter(prisma),
  allowDangerousEmailAccountLinking: true, // Didn't work
  providers: [...]
}
```

TypeScript removed this property because it's not in the type definition.

### Attempt 2: Route Handler

```typescript
const handler = NextAuth({
  ...authOptions,
  allowDangerousEmailAccountLinking: true, // Still didn't work
});
```

The setting wasn't being recognized at runtime.

## What Actually Worked

The key is to set `allowDangerousEmailAccountLinking` **at the provider level**:

```typescript
export const authOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    KeycloakProvider({
      clientId: process.env.KEYCLOAK_CLIENT_ID || "",
      clientSecret: process.env.KEYCLOAK_CLIENT_SECRET || "",
      issuer: process.env.KEYCLOAK_ISSUER || "",
      allowDangerousEmailAccountLinking: true, // This works!
    }),
  ],
} as any; // Use 'as any' to bypass TypeScript checking
```

### Extending the Type Definition

To make TypeScript happy, add this type extension:

```typescript
declare module "next-auth" {
  interface NextAuthOptions {
    allowDangerousEmailAccountLinking?: boolean;
  }
}
```

## Custom Adapter for Better Account Linking

We also created a custom adapter to handle account linking more gracefully:

```typescript
const customAdapter = {
  ...PrismaAdapter(prisma),

  async getUserByEmail(email: string) {
    return await prisma.user.findUnique({
      where: { email },
      include: { accounts: true },
    });
  },

  async linkAccount(account: any) {
    try {
      // Check if account already exists
      const existing = await prisma.account.findUnique({
        where: {
          provider_providerAccountId: {
            provider: account.provider,
            providerAccountId: account.providerAccountId,
          },
        },
      });

      if (existing) {
        // Update existing account
        return await prisma.account.update({
          where: { id: existing.id },
          data: {
            access_token: account.access_token,
            refresh_token: account.refresh_token,
            expires_at: account.expires_at,
          },
        });
      }

      // Create new account
      return await prisma.account.create({
        data: {
          userId: account.userId,
          type: account.type,
          provider: account.provider,
          providerAccountId: account.providerAccountId,
          access_token: account.access_token,
          refresh_token: account.refresh_token,
          expires_at: account.expires_at,
        },
      });
    } catch (error: any) {
      // Handle unique constraint violations
      if (error.code === 'P2002') {
        const existing = await prisma.account.findUnique({
          where: {
            provider_providerAccountId: {
              provider: account.provider,
              providerAccountId: account.providerAccountId,
            },
          },
        });

        if (existing) {
          return await prisma.account.update({
            where: { id: existing.id },
            data: {
              userId: account.userId,
              access_token: account.access_token,
              refresh_token: account.refresh_token,
              expires_at: account.expires_at,
            },
          });
        }
      }
      throw error;
    }
  },
} as Adapter;
```

## Dependencies Issue

We also encountered a compatibility issue with Prisma v7. The solution was to update the adapter:

```bash
npm install @auth/prisma-adapter@latest
npx prisma generate
```

This updated from version 2.8.0 to 2.11.1, which fixed module resolution errors.

## When Is This Safe?

This approach is safe when:

1. You're using OAuth providers that verify email ownership (Google, GitHub, Keycloak with verification)
2. You have database constraints to prevent duplicate accounts
3. You log account linking events for auditing

## Complete Working Example

```typescript
// src/lib/next-auth-options.ts
import { PrismaAdapter } from "@auth/prisma-adapter";
import KeycloakProvider from "next-auth/providers/keycloak";
import { prisma } from "@/lib/prisma";

export const authOptions = {
  debug: process.env.NEXTAUTH_DEBUG === 'true',
  adapter: PrismaAdapter(prisma),
  allowDangerousEmailAccountLinking: true,

  providers: [
    KeycloakProvider({
      clientId: process.env.KEYCLOAK_CLIENT_ID || "",
      clientSecret: process.env.KEYCLOAK_CLIENT_SECRET || "",
      issuer: process.env.KEYCLOAK_ISSUER || "",
      allowDangerousEmailAccountLinking: true,
    }),
  ],

  callbacks: {
    async signIn({ user, account }) {
      if (process.env.NEXTAUTH_DEBUG === 'true') {
        console.log("[Auth] Sign in:", {
          email: user.email,
          provider: account?.provider
        });
      }
      return true;
    },
  },

  events: {
    async linkAccount(message) {
      console.log("[Auth] Account linked:", {
        provider: message.account?.provider,
        userId: message.user?.id
      });
    },
  },

  pages: {
    signIn: "/auth/login",
    error: "/auth/error",
  },

  session: {
    strategy: "jwt",
    maxAge: 30 * 24 * 60 * 60,
  },
} as any;

declare module "next-auth" {
  interface NextAuthOptions {
    allowDangerousEmailAccountLinking?: boolean;
  }
}
```

## Testing

1. Sign in with one provider (e.g., Google)
2. Sign out
3. Sign in with a different provider (e.g., Keycloak) using the same email
4. Check your database - both accounts should be linked to the same user

## Summary

The fix requires two changes:
1. Add `allowDangerousEmailAccountLinking: true` to each provider configuration
2. Use `as any` for the authOptions export to preserve the property at runtime

This enables users to sign in with multiple OAuth providers using the same email address.

---

*For more details, see our [changelog](https://edesy.in/changelog).*
