diff --git a/docs/content/docs/concepts/users-accounts.mdx b/docs/content/docs/concepts/users-accounts.mdx index 65b33dbeb5..367196fa4a 100644 --- a/docs/content/docs/concepts/users-accounts.mdx +++ b/docs/content/docs/concepts/users-accounts.mdx @@ -122,28 +122,37 @@ The account table stores the authentication data of the user. The account table ### Account linking -Account linking allows users to link multiple authentication methods to the same account. This is useful when users want to sign in using different methods. +Account linking allows users to connect multiple authentication methods to a single account. Better Auth supports linking social sign-on or OAuth providers to an existing account if the provider returns a verified email. If the email isn't verified, the account can't be linked. -By default Better Auth allows account linking for all providers. You can disable account linking by setting `accountLinking.enabled` to `false`. ```ts const auth = new BetterAuth({ account: { - accountLinking: { + accountLinking: { enabled: false, - } + } }, }); ``` -You can also specify, the list of providers that should be trusted. When a user signs in using a trusted provider, if the provider returns a verfied email, the user is automatically signed in. +You can also specify a list of trusted providers. If a user signs in using a trusted provider, the account is linked, even if the email verification status is not provided. + +If account linking is disabled, accounts cannot be linked, regardless of the provider or email verification. ```ts const auth = new BetterAuth({ account: { - accountLinking: { + accountLinking: { enabled: true, - trustedProviders: ["email-password", "google"], - } + trustedProviders: ["google", "github"], + } }, }); ``` + +To link a credential-based account (email and password), users can either go through the "forgot password" flow or use the `setPassword` function if they are already logged in. + +```ts +await authClient.user.setPassword({ + password, +}); +``` \ No newline at end of file diff --git a/packages/better-auth/src/api/routes/callback.ts b/packages/better-auth/src/api/routes/callback.ts index f18e3f4567..007b5e5f4d 100644 --- a/packages/better-auth/src/api/routes/callback.ts +++ b/packages/better-auth/src/api/routes/callback.ts @@ -1,4 +1,3 @@ -import { APIError } from "better-call"; import { z } from "zod"; import { userSchema } from "../../db/schema"; import { generateId } from "../../utils/id"; @@ -115,7 +114,15 @@ export const callbackOAuth = createAuthEndpoint( `${c.context.baseURL}/error?error=please_restart_the_process`, ); } - //find user in db + + function redirectOnError(error: string) { + throw c.redirect( + `${ + currentURL || callbackURL || `${c.context.baseURL}/error` + }?error=${error}`, + ); + } + const dbUser = await c.context.internalAdapter .findUserByEmail(user.email, { includeAccounts: true, @@ -141,17 +148,11 @@ export const callbackOAuth = createAuthEndpoint( const isTrustedProvider = trustedProviders?.includes( provider.id as "apple", ); - if (!isTrustedProvider) { - let url: URL; - try { - url = new URL(currentURL || callbackURL); - url.searchParams.set("error", "account_not_linked"); - } catch (e) { - throw c.redirect( - `${c.context.baseURL}/error?error=account_not_linked`, - ); - } - throw c.redirect(url.toString()); + if ( + (!isTrustedProvider && !user.emailVerified) || + !c.context.options.account?.accountLinking?.enabled + ) { + redirectOnError("account_not_linked"); } try { await c.context.internalAdapter.linkAccount({ @@ -162,10 +163,8 @@ export const callbackOAuth = createAuthEndpoint( ...getAccountTokens(tokens), }); } catch (e) { - console.log(e); - throw c.redirect( - `${c.context.baseURL}/error?error=failed_linking_account`, - ); + logger.error("Unable to link account", e); + redirectOnError("unable_to_link_account"); } } } else { @@ -199,40 +198,23 @@ export const callbackOAuth = createAuthEndpoint( ); } } catch (e) { - const url = new URL(currentURL || callbackURL); - url.searchParams.set("error", "unable_to_create_user"); - throw c.redirect(url.toString()); + logger.error("Unable to create user", e); + redirectOnError("unable_to_create_user"); } } - //this should never happen - if (!userId && !id) - throw new APIError("INTERNAL_SERVER_ERROR", { - message: "Unable to create user", - }); - //create session - try { - const session = await c.context.internalAdapter.createSession( - userId || id, - c.request, - ); - if (!session) { - const url = new URL(currentURL || callbackURL); - url.searchParams.set("error", "unable_to_create_session"); - throw c.redirect(url.toString()); - } - try { - await setSessionCookie(c, session.id); - } catch (e) { - c.context.logger.error("Unable to set session cookie", e); - const url = new URL(currentURL || callbackURL); - url.searchParams.set("error", "unable_to_create_session"); - throw c.redirect(url.toString()); - } - } catch { - const url = new URL(currentURL || callbackURL || ""); - url.searchParams.set("error", "unable_to_create_session"); - throw c.redirect(url.toString()); + + if (!userId && !id) { + redirectOnError("unable_to_create_user"); } + + const session = await c.context.internalAdapter.createSession( + userId || id, + c.request, + ); + if (!session) { + redirectOnError("unable_to_create_session"); + } + await setSessionCookie(c, session.id); throw c.redirect(callbackURL); }, ); diff --git a/packages/better-auth/src/api/routes/update-user.test.ts b/packages/better-auth/src/api/routes/update-user.test.ts index ba0faef465..e2b7265547 100644 --- a/packages/better-auth/src/api/routes/update-user.test.ts +++ b/packages/better-auth/src/api/routes/update-user.test.ts @@ -1,7 +1,5 @@ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; import { getTestInstance } from "../../test-utils/test-instance"; -import { createAuthClient } from "../../client"; -import { parseSetCookieHeader } from "../../cookies"; describe("updateUser", async () => { let emailVerificationToken: string;