better-auth
Version:
The most comprehensive authentication framework for TypeScript.
178 lines (177 loc) • 8.14 kB
JavaScript
import { isAPIError } from "../utils/is-api-error.mjs";
import { parseAdditionalUserInputFromProviderProfile } from "../db/schema.mjs";
import { setAccountCookie } from "../cookies/session-store.mjs";
import { redirectOnError } from "./errors.mjs";
import { setTokenUtil } from "./utils.mjs";
import { createEmailVerificationToken } from "../api/routes/email-verification.mjs";
import { isDevelopment } from "@better-auth/core/env";
//#region src/oauth2/link-account.ts
async function handleOAuthUserInfo(c, opts) {
const { userInfo, account, callbackURL, disableSignUp, overrideUserInfo } = opts;
const dbUser = await c.context.internalAdapter.findOAuthUser(userInfo.email.toLowerCase(), account.accountId, account.providerId).catch((e) => {
c.context.logger.error("Better auth was unable to query your database.\nError: ", e);
redirectOnError(c, c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`, "internal_server_error");
});
let user = dbUser?.user;
const isRegister = !user;
if (dbUser) {
const linkedAccount = dbUser.linkedAccount ?? dbUser.accounts.find((acc) => acc.providerId === account.providerId && acc.accountId === account.accountId);
if (!linkedAccount) {
const accountLinking = c.context.options.account?.accountLinking;
const isTrustedProvider = opts.isTrustedProvider || opts.trustProviderByName !== false && c.context.trustedProviders.includes(account.providerId);
const requireLocalEmailVerified = accountLinking?.requireLocalEmailVerified ?? true;
if (!isTrustedProvider && !userInfo.emailVerified || requireLocalEmailVerified && !dbUser.user.emailVerified || accountLinking?.enabled === false || accountLinking?.disableImplicitLinking === true) {
if (isDevelopment()) c.context.logger.warn(`User already exist but account isn't linked to ${account.providerId}. To read more about how account linking works in Better Auth see https://www.better-auth.com/docs/concepts/users-accounts#account-linking.`);
return {
error: "account not linked",
data: null
};
}
try {
await c.context.internalAdapter.linkAccount({
providerId: account.providerId,
accountId: userInfo.id.toString(),
userId: dbUser.user.id,
accessToken: await setTokenUtil(account.accessToken, c.context),
refreshToken: await setTokenUtil(account.refreshToken, c.context),
idToken: account.idToken,
accessTokenExpiresAt: account.accessTokenExpiresAt,
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
scope: account.scope
});
} catch (e) {
c.context.logger.error("Unable to link account", e);
return {
error: "unable to link account",
data: null
};
}
if (userInfo.emailVerified && !dbUser.user.emailVerified && userInfo.email.toLowerCase() === dbUser.user.email) await c.context.internalAdapter.updateUser(dbUser.user.id, { emailVerified: true });
user = await applyUpdateUserInfoOnLink(c, dbUser.user.id, userInfo) ?? user;
} else {
const freshTokens = c.context.options.account?.updateAccountOnSignIn !== false ? Object.fromEntries(Object.entries({
idToken: account.idToken,
accessToken: await setTokenUtil(account.accessToken, c.context),
refreshToken: await setTokenUtil(account.refreshToken, c.context),
accessTokenExpiresAt: account.accessTokenExpiresAt,
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
scope: account.scope
}).filter(([_, value]) => value !== void 0)) : {};
if (c.context.options.account?.storeAccountCookie) await setAccountCookie(c, {
...linkedAccount,
...freshTokens
});
if (Object.keys(freshTokens).length > 0) await c.context.internalAdapter.updateAccount(linkedAccount.id, freshTokens);
if (userInfo.emailVerified && !dbUser.user.emailVerified && userInfo.email.toLowerCase() === dbUser.user.email) await c.context.internalAdapter.updateUser(dbUser.user.id, { emailVerified: true });
}
if (overrideUserInfo) {
const { id: _id, email: _email, emailVerified: _emailVerified, name, image, ...providerProfile } = userInfo;
const additionalUserFields = parseAdditionalUserInputFromProviderProfile(c.context.options, providerProfile, "update");
user = await c.context.internalAdapter.updateUser(dbUser.user.id, {
name,
image,
...additionalUserFields,
email: userInfo.email.toLowerCase(),
emailVerified: userInfo.email.toLowerCase() === dbUser.user.email ? dbUser.user.emailVerified || userInfo.emailVerified : userInfo.emailVerified
});
}
} else {
if (disableSignUp) return {
error: "signup disabled",
data: null,
isRegister: false
};
try {
const { id: _id, email: _email, emailVerified: _emailVerified, name, image, ...providerProfile } = userInfo;
const additionalUserFields = parseAdditionalUserInputFromProviderProfile(c.context.options, providerProfile, "create");
const accountData = {
accessToken: await setTokenUtil(account.accessToken, c.context),
refreshToken: await setTokenUtil(account.refreshToken, c.context),
idToken: account.idToken,
accessTokenExpiresAt: account.accessTokenExpiresAt,
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
scope: account.scope,
providerId: account.providerId,
accountId: userInfo.id.toString()
};
const { user: createdUser, account: createdAccount } = await c.context.internalAdapter.createOAuthUser({
name,
image,
...additionalUserFields,
email: userInfo.email.toLowerCase(),
emailVerified: userInfo.emailVerified
}, accountData);
user = createdUser;
if (c.context.options.account?.storeAccountCookie) await setAccountCookie(c, createdAccount);
if (!userInfo.emailVerified && user && c.context.options.emailVerification?.sendOnSignUp && c.context.options.emailVerification?.sendVerificationEmail) {
const token = await createEmailVerificationToken(c.context.secret, user.email, void 0, c.context.options.emailVerification?.expiresIn);
const url = `${c.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(callbackURL || "/")}`;
await c.context.runInBackgroundOrAwait(c.context.options.emailVerification.sendVerificationEmail({
user,
url,
token
}, c.request));
}
} catch (e) {
c.context.logger.error(e);
if (isAPIError(e)) return {
error: e.message,
data: null,
isRegister: false
};
return {
error: "unable to create user",
data: null,
isRegister: false
};
}
}
if (!user) return {
error: "unable to create user",
data: null,
isRegister: false
};
const session = await c.context.internalAdapter.createSession(user.id);
if (!session) return {
error: "unable to create session",
data: null,
isRegister: false
};
return {
data: {
session,
user
},
error: null,
isRegister
};
}
/**
* Apply the `account.accountLinking.updateUserInfoOnLink` policy: when enabled,
* copy the freshly linked provider's profile onto the local user, matching the
* field set persisted on sign-up. The local `email` and `emailVerified` are
* never changed, so a link can't rebind the account's identity, and
* `updateUser` drops `undefined` fields, so a provider that omits one leaves
* the existing column intact.
*
* Returns the updated user so a caller that issues a session can seed the
* cookie cache with the fresh row. Returns `undefined` when the policy is
* disabled or the update fails: a failed profile sync must not abort the link.
*/
async function applyUpdateUserInfoOnLink(c, userId, userInfo) {
if (c.context.options.account?.accountLinking?.updateUserInfoOnLink !== true) return;
try {
const { id: _id, email: _email, emailVerified: _emailVerified, name, image, ...providerProfile } = userInfo;
const additionalUserFields = parseAdditionalUserInputFromProviderProfile(c.context.options, providerProfile, "update");
return await c.context.internalAdapter.updateUser(userId, {
name,
image,
...additionalUserFields
});
} catch (e) {
c.context.logger.warn("Could not update user info on account link", e);
return;
}
}
//#endregion
export { applyUpdateUserInfoOnLink, handleOAuthUserInfo };