[GH-ISSUE #5457] Cannot transfer previously linked OAuth account to current user #27574

Closed
opened 2026-04-17 18:39:32 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @marcospgp on GitHub (Oct 21, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/5457

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Use Case

Users may have linked their Discord/Google/etc account to an old account they no longer use. When they try
linking to their current account, they get account_already_linked_to_different_user error and have to:

  1. Remember which account has the OAuth link
  2. Log into that old account
  3. Unlink manually
  4. Log back into current account
  5. Try linking again

This is poor UX when the user clearly owns the OAuth account (they just completed OAuth flow successfully).

Current Behavior

  1. User B clicks "Link Discord"
  2. Completes Discord OAuth ✓ (proves ownership)
  3. BetterAuth callback checks if Discord ID already linked to User A
  4. Redirects to /api/auth/error?error=account_already_linked_to_different_user
  5. No account created

Desired Behavior

Auto-transfer when OAuth succeeds:

  1. User B clicks "Link Discord"
  2. Completes Discord OAuth ✓ (proves ownership)
  3. BetterAuth callback checks if Discord ID already linked to User A
  4. Since OAuth succeeded (proves User B owns this Discord account):
    • Unlink from User A automatically
    • Link to User B
  5. Success

Why This Is Secure

  • OAuth completion proves ownership - User proved they control the Discord account by completing OAuth flow
  • Time-limited - Can't reuse old OAuth sessions; must complete fresh OAuth
  • Session-bound - User must be authenticated to their current account
  • No user input - No client-provided parameters to spoof

Suggested Implementation

Add config option to account linking:

account: {
accountLinking: {
enabled: true,
autoTransfer: true, // NEW: auto-transfer from other user if OAuth succeeds
allowDifferentEmails: true,
}
}

Or add a hook/callback that fires after OAuth succeeds but before "already linked" check:

databaseHooks: {
account: {
beforeLinkCheck: async (account, existingLink) => {
if (existingLink && existingLink.userId !== account.userId) {
// Custom logic: auto-transfer, prompt user, etc.
return { shouldTransfer: true };
}
}
}
}

Workarounds Attempted

  1. Database hook (databaseHooks.account.create.before):
    Doesn't fire because BetterAuth checks "already linked" BEFORE attempting account creation, so the database
    hook never runs.

  2. Endpoint hook (hooks.before):
    hooks: {
    before: createAuthMiddleware(async (ctx) => {
    if (ctx.path === "/callback/:id") {
    // We have the OAuth code here but not the Discord user ID yet
    // BetterAuth does code exchange internally AFTER this hook
    // Would need to duplicate OAuth code exchange to get user ID
    // Then check/unlink before BetterAuth does its checks
    }
    })
    }
    This works but requires duplicating the OAuth token exchange that BetterAuth does internally, which is
    inefficient and error-prone.

Alternative

If auto-transfer is too opinionated, just exposing a hook at the right point in the flow (after OAuth code
exchange, before "already linked" check) would let users implement their own logic without duplicating OAuth
exchanges.

the key need is being able to transfer an account after the current user has proved ownership but before the "already linked" error is triggered

Current vs. Expected behavior

see above

What version of Better Auth are you using?

1.3.28

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.5.0: Tue Apr 22 19:54:29 PDT 2025; root:xnu-11417.121.6~2/RELEASE_ARM64_T6030",
    "release": "24.5.0",
    "cpuCount": 11,
    "cpuModel": "Apple M3 Pro",
    "totalMemory": "18.00 GB",
    "freeMemory": "0.16 GB"
  },
  "node": {
    "version": "v22.16.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.9.2"
  },
  "frameworks": [
    {
      "name": "react",
      "version": "^19.2.0"
    },
    {
      "name": "hono",
      "version": "^4.10.1"
    }
  ],
  "databases": [
    {
      "name": "pg",
      "version": "^8.16.3"
    },
    {
      "name": "drizzle",
      "version": "^0.44.6"
    }
  ],
  "betterAuth": {
    "version": "^1.3.28",
    "config": {
      "telemetry": {
        "enabled": false
      },
      "plugins": [
        {
          "name": "last-login-method",
          "config": {
            "id": "last-login-method",
            "hooks": {
              "after": [
                {}
              ]
            }
          }
        }
      ],
      "hooks": {},
      "user": {
        "deleteUser": {
          "enabled": true
        }
      },
      "account": {
        "accountLinking": {
          "enabled": true,
          "allowDifferentEmails": true
        }
      },
      "socialProviders": {
        "github": {
          "clientId": "[REDACTED]",
          "clientSecret": "[REDACTED]"
        },
        "google": {
          "clientId": "[REDACTED]",
          "clientSecret": "[REDACTED]"
        },
        "twitter": {
          "clientId": "[REDACTED]",
          "clientSecret": "[REDACTED]"
        },
        "discord": {
          "clientId": "[REDACTED]",
          "clientSecret": "[REDACTED]",
          "scope": [
            "identify",
            "guilds"
          ]
        }
      },
      "databaseHooks": {
        "account": {
          "create": {},
          "update": {}
        },
        "user": {
          "create": {}
        }
      }
    }
  }
}

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

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
});

Additional context

No response

Originally created by @marcospgp on GitHub (Oct 21, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/5457 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce Use Case Users may have linked their Discord/Google/etc account to an old account they no longer use. When they try linking to their current account, they get account_already_linked_to_different_user error and have to: 1. Remember which account has the OAuth link 2. Log into that old account 3. Unlink manually 4. Log back into current account 5. Try linking again This is poor UX when the user clearly owns the OAuth account (they just completed OAuth flow successfully). Current Behavior 1. User B clicks "Link Discord" 2. Completes Discord OAuth ✓ (proves ownership) 3. BetterAuth callback checks if Discord ID already linked to User A 4. Redirects to /api/auth/error?error=account_already_linked_to_different_user 5. No account created Desired Behavior Auto-transfer when OAuth succeeds: 1. User B clicks "Link Discord" 2. Completes Discord OAuth ✓ (proves ownership) 3. BetterAuth callback checks if Discord ID already linked to User A 4. Since OAuth succeeded (proves User B owns this Discord account): - Unlink from User A automatically - Link to User B 5. Success Why This Is Secure - OAuth completion proves ownership - User proved they control the Discord account by completing OAuth flow - Time-limited - Can't reuse old OAuth sessions; must complete fresh OAuth - Session-bound - User must be authenticated to their current account - No user input - No client-provided parameters to spoof Suggested Implementation Add config option to account linking: account: { accountLinking: { enabled: true, autoTransfer: true, // NEW: auto-transfer from other user if OAuth succeeds allowDifferentEmails: true, } } Or add a hook/callback that fires after OAuth succeeds but before "already linked" check: databaseHooks: { account: { beforeLinkCheck: async (account, existingLink) => { if (existingLink && existingLink.userId !== account.userId) { // Custom logic: auto-transfer, prompt user, etc. return { shouldTransfer: true }; } } } } Workarounds Attempted 1. Database hook (databaseHooks.account.create.before): Doesn't fire because BetterAuth checks "already linked" BEFORE attempting account creation, so the database hook never runs. 2. Endpoint hook (hooks.before): hooks: { before: createAuthMiddleware(async (ctx) => { if (ctx.path === "/callback/:id") { // We have the OAuth code here but not the Discord user ID yet // BetterAuth does code exchange internally AFTER this hook // Would need to duplicate OAuth code exchange to get user ID // Then check/unlink before BetterAuth does its checks } }) } This works but requires duplicating the OAuth token exchange that BetterAuth does internally, which is inefficient and error-prone. Alternative If auto-transfer is too opinionated, just exposing a hook at the right point in the flow (after OAuth code exchange, before "already linked" check) would let users implement their own logic without duplicating OAuth exchanges. the key need is being able to transfer an account **after** the current user has proved ownership but **before** the "already linked" error is triggered ### Current vs. Expected behavior see above ### What version of Better Auth are you using? 1.3.28 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.5.0: Tue Apr 22 19:54:29 PDT 2025; root:xnu-11417.121.6~2/RELEASE_ARM64_T6030", "release": "24.5.0", "cpuCount": 11, "cpuModel": "Apple M3 Pro", "totalMemory": "18.00 GB", "freeMemory": "0.16 GB" }, "node": { "version": "v22.16.0", "env": "development" }, "packageManager": { "name": "npm", "version": "10.9.2" }, "frameworks": [ { "name": "react", "version": "^19.2.0" }, { "name": "hono", "version": "^4.10.1" } ], "databases": [ { "name": "pg", "version": "^8.16.3" }, { "name": "drizzle", "version": "^0.44.6" } ], "betterAuth": { "version": "^1.3.28", "config": { "telemetry": { "enabled": false }, "plugins": [ { "name": "last-login-method", "config": { "id": "last-login-method", "hooks": { "after": [ {} ] } } } ], "hooks": {}, "user": { "deleteUser": { "enabled": true } }, "account": { "accountLinking": { "enabled": true, "allowDifferentEmails": true } }, "socialProviders": { "github": { "clientId": "[REDACTED]", "clientSecret": "[REDACTED]" }, "google": { "clientId": "[REDACTED]", "clientSecret": "[REDACTED]" }, "twitter": { "clientId": "[REDACTED]", "clientSecret": "[REDACTED]" }, "discord": { "clientId": "[REDACTED]", "clientSecret": "[REDACTED]", "scope": [ "identify", "guilds" ] } }, "databaseHooks": { "account": { "create": {}, "update": {} }, "user": { "create": {} } } } } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); ``` ### Additional context _No response_
GiteaMirror added the locked label 2026-04-17 18:39:32 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Oct 21, 2025):

Better Auth does not currently support automatic transfer of an OAuth account from one user to another after ownership is proven via OAuth flow. There is no built-in config option (like autoTransfer) or hook (such as beforeLinkCheck) that fires after the OAuth code exchange but before the account_already_linked_to_different_user error is triggered. The error is enforced immediately after user info is retrieved from the OAuth provider, and no extensibility point exists at that stage for custom logic or auto-transfer. All existing hooks (like databaseHooks.account.create.before) run either before the OAuth exchange or after account creation, so they can't be used for this purpose. Current workarounds require either manual unlinking or duplicating the OAuth code exchange outside of Better Auth, which is inefficient and error-prone reference reference.

There is ongoing community interest in adding a hook or config option for this use case, but as of v1.3.28, it is not implemented.

To reply, just mention @dosu.


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

<!-- gh-comment-id:3426580500 --> @dosubot[bot] commented on GitHub (Oct 21, 2025): <!-- Answer --> Better Auth does not currently support automatic transfer of an OAuth account from one user to another after ownership is proven via OAuth flow. There is no built-in config option (like `autoTransfer`) or hook (such as `beforeLinkCheck`) that fires after the OAuth code exchange but before the `account_already_linked_to_different_user` error is triggered. The error is enforced immediately after user info is retrieved from the OAuth provider, and no extensibility point exists at that stage for custom logic or auto-transfer. All existing hooks (like `databaseHooks.account.create.before`) run either before the OAuth exchange or after account creation, so they can't be used for this purpose. Current workarounds require either manual unlinking or duplicating the OAuth code exchange outside of Better Auth, which is inefficient and error-prone [reference](https://github.com/better-auth/better-auth/issues/1834) [reference](https://github.com/better-auth/better-auth/issues/2327). There is ongoing community interest in adding a hook or config option for this use case, but as of v1.3.28, it is not implemented. <!-- 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/4ff589db-e231-406d-95b2-b8a577e7bf58?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/4ff589db-e231-406d-95b2-b8a577e7bf58?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/4ff589db-e231-406d-95b2-b8a577e7bf58?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/4ff589db-e231-406d-95b2-b8a577e7bf58?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/4ff589db-e231-406d-95b2-b8a577e7bf58?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/4ff589db-e231-406d-95b2-b8a577e7bf58?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/4ff589db-e231-406d-95b2-b8a577e7bf58?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/5457)
Author
Owner

@jovanialferez commented on GitHub (Feb 4, 2026):

@marcospgp were you able to find a solid solution or workaround to this? i am on the same situation as well. thanks!

<!-- gh-comment-id:3845617366 --> @jovanialferez commented on GitHub (Feb 4, 2026): @marcospgp were you able to find a solid solution or workaround to this? i am on the same situation as well. thanks!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#27574