allowDifferentEmails: false still allows linking accounts with different emails #1908

Closed
opened 2026-03-13 09:12:10 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @Rayyan-Balami on GitHub (Sep 12, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Describe the bug

When using Better Auth with account.accountLinking.enabled = true and allowDifferentEmails: false, I am still able to link multiple OAuth accounts with different email addresses.

Additionally, when listing linked accounts (via listAccounts()), the email for each account is not included, making it impossible to display which email is linked to which provider.

To Reproduce

  1. Sign up with email/password using email X (or sign in with Google account with email X).
  2. Link a Google account with email X → works fine.
  3. Unlink Google (email X).
  4. Link another Google account with different email Y.
  5. Linking succeeds, even though allowDifferentEmails: false.
  6. Call authClient.user.listAccounts() → email for each linked account is missing.

Expected behavior

  • Better Auth should prevent linking if the new account's email is different from the existing user email, since allowDifferentEmails is explicitly set to false.
  • listAccounts() should return email and other necessary fields for each linked account (not just accountId and provider), so the UI can display the associated email.

Config snippet

export const auth = betterAuth({
  database: mongodbAdapter(client),
  secret: BETTER_AUTH_SECRET,
  baseURL: BETTER_AUTH_URL,
  account: {
    accountLinking: {
      enabled: true,
      allowDifferentEmails: false,
      // updateUserInfoOnLink: true also tested
    },
  },
  socialProviders: {
    google: {
      prompt: "select_account",
      clientId: GOOGLE_CLIENT_ID,
      clientSecret: GOOGLE_CLIENT_SECRET,
      scope: ["openid", "email", "profile"],
    },
    microsoft: {
      prompt: "select_account",
      clientId: MICROSOFT_CLIENT_ID,
      clientSecret: MICROSOFT_CLIENT_SECRET,
      tenantId: "common",
      scope: ["openid", "email", "profile"],
    },
  },
});


### Current vs. Expected behavior

Current behavior:
Even with allowDifferentEmails: false, Better Auth allows linking multiple OAuth accounts with different email addresses.
When calling listAccounts(), the email for each linked account is not returned, so its impossible to know which email is associated with each provider.
Expected behavior:
Linking a new OAuth account with an email different from the users existing email should be blocked when allowDifferentEmails: false.
listAccounts() should return the email and other necessary fields (like name or picture) for each linked account, so the UI can display which email is linked to which provider.

### What version of Better Auth are you using?

"better-auth": "^1.3.7",

### System info

```bash
{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:34 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T8103",
    "release": "24.6.0",
    "cpuCount": 8,
    "cpuModel": "Apple M1",
    "totalMemory": "8.00 GB",
    "freeMemory": "0.17 GB"
  },
  "node": {
    "version": "v22.17.1",
    "env": "development"
  },
  "packageManager": {
    "name": "pnpm",
    "version": "10.15.0"
  },
  "frameworks": null,
  "databases": null,
  "betterAuth": {
    "version": "Unknown",
    "config": null,
    "error": "ENOENT: no such file or directory, open 'package.json'"
  }
}


using MERN-orpc, monorepo, from better-t-stack,

Which area(s) are affected? (Select all that apply)

Backend, Client

Auth config (if applicable)

import { client } from "@/lib/mongo";
import { betterAuth } from "better-auth";
import { mongodbAdapter } from "better-auth/adapters/mongodb";
import {
  BETTER_AUTH_SECRET,
  BETTER_AUTH_URL,
  CORS_ORIGIN,
  GOOGLE_CLIENT_ID,
  GOOGLE_CLIENT_SECRET,
  IS_PROD,
  MICROSOFT_CLIENT_ID,
  MICROSOFT_CLIENT_SECRET,
  REQUIRE_EMAIL_VERIFICATION,
} from "@/config/constants";
import { admin } from "better-auth/plugins";
import { sendResetPasswordEmail, sendVerificationEmail } from "./resend";
import { userRoleEnum } from "@codebase/shared/schemas";

export const auth = betterAuth({
  database: mongodbAdapter(client),
  secret: BETTER_AUTH_SECRET,
  baseURL: BETTER_AUTH_URL,
  trustedOrigins: [CORS_ORIGIN],
  account: {
    accountLinking: {
      enabled: true,
      allowDifferentEmails: true, // 🚫 forbid different emails
      updateUserInfoOnLink: true, // ✅ keep user.email in sync
      trustedProviders: ["google", "microsoft"], // optional whitelist
    },
  },
  plugins: [
    admin({
      defaultRole: userRoleEnum.USER,
      adminRoles: [userRoleEnum.ADMIN],
    }),
  ],
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: REQUIRE_EMAIL_VERIFICATION,
    sendResetPassword: async ({ user, url, token }, request) => {
      await sendResetPasswordEmail(user, url);
    },
  },
  emailVerification: {
    sendOnSignUp: true,
    autoSignInAfterVerification: true,
    sendVerificationEmail: async ({ user, url, token }, request) => {
      await sendVerificationEmail(user, url);
    },
  },
  session: {
    expiresIn: 60 * 60 * 24 * 7, // 7 days
    updateAge: 60 * 60 * 24, // 1 day
  },
  advanced: {
    defaultCookieAttributes: {
      sameSite: "lax",
      secure: IS_PROD,
      httpOnly: true,
    },
  },
  socialProviders: {
    google: {
      prompt: "select_account",
      clientId: GOOGLE_CLIENT_ID,
      clientSecret: GOOGLE_CLIENT_SECRET,
      scope: ["openid", "email", "profile"],
    },
    microsoft: {
      prompt: "select_account",
      clientId: MICROSOFT_CLIENT_ID,
      clientSecret: MICROSOFT_CLIENT_SECRET,
      tenantId: "common",
      scope: ["openid", "email", "profile"],
    },
  },
});

// Use the shared types for consistency
export type Session = typeof auth.$Infer.Session.session;
export type User = typeof auth.$Infer.Session.user;

Additional context

I am using better auth for the first time !

Originally created by @Rayyan-Balami on GitHub (Sep 12, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce ### Describe the bug When using Better Auth with `account.accountLinking.enabled = true` and `allowDifferentEmails: false`, I am still able to link multiple OAuth accounts with **different email addresses**. Additionally, when listing linked accounts (via `listAccounts()`), the email for each account is **not included**, making it impossible to display which email is linked to which provider. ### To Reproduce 1. Sign up with email/password using email **X** (or sign in with Google account with email **X**). 2. Link a Google account with email **X** → works fine. 3. Unlink Google (email X). 4. Link another Google account with **different email Y**. 5. Linking **succeeds**, even though `allowDifferentEmails: false`. 6. Call `authClient.user.listAccounts()` → email for each linked account is missing. ### Expected behavior - Better Auth should **prevent linking** if the new account's email is different from the existing user email, since `allowDifferentEmails` is explicitly set to `false`. - `listAccounts()` should return email and other necessary fields for each linked account (not just `accountId` and `provider`), so the UI can display the associated email. ### Config snippet ```ts export const auth = betterAuth({ database: mongodbAdapter(client), secret: BETTER_AUTH_SECRET, baseURL: BETTER_AUTH_URL, account: { accountLinking: { enabled: true, allowDifferentEmails: false, // updateUserInfoOnLink: true also tested }, }, socialProviders: { google: { prompt: "select_account", clientId: GOOGLE_CLIENT_ID, clientSecret: GOOGLE_CLIENT_SECRET, scope: ["openid", "email", "profile"], }, microsoft: { prompt: "select_account", clientId: MICROSOFT_CLIENT_ID, clientSecret: MICROSOFT_CLIENT_SECRET, tenantId: "common", scope: ["openid", "email", "profile"], }, }, }); ### Current vs. Expected behavior Current behavior: Even with allowDifferentEmails: false, Better Auth allows linking multiple OAuth accounts with different email addresses. When calling listAccounts(), the email for each linked account is not returned, so it’s impossible to know which email is associated with each provider. Expected behavior: Linking a new OAuth account with an email different from the user’s existing email should be blocked when allowDifferentEmails: false. listAccounts() should return the email and other necessary fields (like name or picture) for each linked account, so the UI can display which email is linked to which provider. ### What version of Better Auth are you using? "better-auth": "^1.3.7", ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:34 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T8103", "release": "24.6.0", "cpuCount": 8, "cpuModel": "Apple M1", "totalMemory": "8.00 GB", "freeMemory": "0.17 GB" }, "node": { "version": "v22.17.1", "env": "development" }, "packageManager": { "name": "pnpm", "version": "10.15.0" }, "frameworks": null, "databases": null, "betterAuth": { "version": "Unknown", "config": null, "error": "ENOENT: no such file or directory, open 'package.json'" } } using MERN-orpc, monorepo, from better-t-stack, ``` ### Which area(s) are affected? (Select all that apply) Backend, Client ### Auth config (if applicable) ```typescript import { client } from "@/lib/mongo"; import { betterAuth } from "better-auth"; import { mongodbAdapter } from "better-auth/adapters/mongodb"; import { BETTER_AUTH_SECRET, BETTER_AUTH_URL, CORS_ORIGIN, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, IS_PROD, MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET, REQUIRE_EMAIL_VERIFICATION, } from "@/config/constants"; import { admin } from "better-auth/plugins"; import { sendResetPasswordEmail, sendVerificationEmail } from "./resend"; import { userRoleEnum } from "@codebase/shared/schemas"; export const auth = betterAuth({ database: mongodbAdapter(client), secret: BETTER_AUTH_SECRET, baseURL: BETTER_AUTH_URL, trustedOrigins: [CORS_ORIGIN], account: { accountLinking: { enabled: true, allowDifferentEmails: true, // 🚫 forbid different emails updateUserInfoOnLink: true, // ✅ keep user.email in sync trustedProviders: ["google", "microsoft"], // optional whitelist }, }, plugins: [ admin({ defaultRole: userRoleEnum.USER, adminRoles: [userRoleEnum.ADMIN], }), ], emailAndPassword: { enabled: true, requireEmailVerification: REQUIRE_EMAIL_VERIFICATION, sendResetPassword: async ({ user, url, token }, request) => { await sendResetPasswordEmail(user, url); }, }, emailVerification: { sendOnSignUp: true, autoSignInAfterVerification: true, sendVerificationEmail: async ({ user, url, token }, request) => { await sendVerificationEmail(user, url); }, }, session: { expiresIn: 60 * 60 * 24 * 7, // 7 days updateAge: 60 * 60 * 24, // 1 day }, advanced: { defaultCookieAttributes: { sameSite: "lax", secure: IS_PROD, httpOnly: true, }, }, socialProviders: { google: { prompt: "select_account", clientId: GOOGLE_CLIENT_ID, clientSecret: GOOGLE_CLIENT_SECRET, scope: ["openid", "email", "profile"], }, microsoft: { prompt: "select_account", clientId: MICROSOFT_CLIENT_ID, clientSecret: MICROSOFT_CLIENT_SECRET, tenantId: "common", scope: ["openid", "email", "profile"], }, }, }); // Use the shared types for consistency export type Session = typeof auth.$Infer.Session.session; export type User = typeof auth.$Infer.Session.user; ``` ### Additional context I am using better auth for the first time !
GiteaMirror added the bug label 2026-03-13 09:12:10 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Sep 12, 2025):

Hi @Rayyan-Balami! I'm Dosu and I’m helping the better-auth team.

The issue with allowDifferentEmails: false not preventing linking accounts with different emails is a known bug in Better Auth v1.3.7. This was fixed in a recent update (>=1.3.8), where the enforcement logic was corrected to always block linking if the emails differ and allowDifferentEmails is false [PR #4504] [Issue #4503].

For listAccounts(), it's a current limitation that it does not return email or other user info for each linked account. If you need to display the email or profile info for linked accounts, you can use the /account-info endpoint to fetch details from the provider using the access token, or extend the account schema with additional fields and hooks [API source] [Feature request].

Upgrading to the latest version should resolve the email linking enforcement bug. For richer account info in your UI, consider the workarounds above until the API is extended to include more fields.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

@dosubot[bot] commented on GitHub (Sep 12, 2025): <!-- Greeting --> Hi @Rayyan-Balami! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The issue with `allowDifferentEmails: false` not preventing linking accounts with different emails is a known bug in Better Auth v1.3.7. This was fixed in a recent update (>=1.3.8), where the enforcement logic was corrected to always block linking if the emails differ and `allowDifferentEmails` is false [[PR #4504](https://github.com/better-auth/better-auth/pull/4504)] [[Issue #4503](https://github.com/better-auth/better-auth/issues/4503)]. For `listAccounts()`, it's a current limitation that it does not return email or other user info for each linked account. If you need to display the email or profile info for linked accounts, you can use the `/account-info` endpoint to fetch details from the provider using the access token, or extend the account schema with additional fields and hooks [[API source](https://github.com/better-auth/better-auth/blob/a5391f8909ee91beae65bac78697df2cf6810fc7/packages/better-auth/src/api/routes/account.ts)] [[Feature request](https://github.com/better-auth/better-auth/issues/2272)]. Upgrading to the latest version should resolve the email linking enforcement bug. For richer account info in your UI, consider the workarounds above until the API is extended to include more fields. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/bd717b8b-fbb1-4e14-a8e5-749c6defb080?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/bd717b8b-fbb1-4e14-a8e5-749c6defb080?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/bd717b8b-fbb1-4e14-a8e5-749c6defb080?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/bd717b8b-fbb1-4e14-a8e5-749c6defb080?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/bd717b8b-fbb1-4e14-a8e5-749c6defb080?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/bd717b8b-fbb1-4e14-a8e5-749c6defb080?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/bd717b8b-fbb1-4e14-a8e5-749c6defb080?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/4610)
Author
Owner

@Bekacru commented on GitHub (Sep 13, 2025):

update to latest

@Bekacru commented on GitHub (Sep 13, 2025): update to latest
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1908