better-auth 1.7.0-beta.7 → 1.7.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/api/index.d.mts +8 -26
  2. package/dist/api/routes/account.d.mts +4 -11
  3. package/dist/api/routes/account.mjs +81 -64
  4. package/dist/api/routes/callback.mjs +35 -30
  5. package/dist/api/routes/sign-in.d.mts +0 -2
  6. package/dist/api/routes/sign-in.mjs +13 -21
  7. package/dist/cookies/session-store.d.mts +1 -1
  8. package/dist/db/get-migration.mjs +1 -34
  9. package/dist/db/internal-adapter.mjs +20 -0
  10. package/dist/db/schema.d.mts +1 -1
  11. package/dist/oauth2/index.d.mts +3 -5
  12. package/dist/oauth2/index.mjs +3 -5
  13. package/dist/oauth2/link-account.d.mts +74 -0
  14. package/dist/oauth2/link-account.mjs +222 -0
  15. package/dist/oauth2/state.d.mts +0 -11
  16. package/dist/oauth2/state.mjs +1 -2
  17. package/dist/oauth2/utils.d.mts +11 -0
  18. package/dist/oauth2/{token-encryption.mjs → utils.mjs} +6 -2
  19. package/dist/package.mjs +1 -1
  20. package/dist/plugins/generic-oauth/index.d.mts +2 -2
  21. package/dist/plugins/generic-oauth/index.mjs +0 -1
  22. package/dist/plugins/oauth-popup/index.mjs +2 -2
  23. package/dist/plugins/oauth-proxy/index.mjs +6 -20
  24. package/dist/plugins/one-tap/index.mjs +6 -10
  25. package/dist/state.d.mts +1 -10
  26. package/dist/state.mjs +1 -2
  27. package/dist/test-utils/http-test-instance.d.mts +0 -20
  28. package/package.json +8 -8
  29. package/dist/oauth2/persist-account.d.mts +0 -80
  30. package/dist/oauth2/persist-account.mjs +0 -84
  31. package/dist/oauth2/resolve-account.d.mts +0 -126
  32. package/dist/oauth2/resolve-account.mjs +0 -128
  33. package/dist/oauth2/sign-in-with-oauth-identity.d.mts +0 -83
  34. package/dist/oauth2/sign-in-with-oauth-identity.mjs +0 -133
  35. package/dist/oauth2/token-encryption.d.mts +0 -7
@@ -1,11 +1,11 @@
1
1
  import { formCsrfMiddleware } from "../middlewares/origin-check.mjs";
2
2
  import { parseUserOutput } from "../../db/schema.mjs";
3
- import { generateRandomString } from "../../crypto/random.mjs";
4
3
  import { setSessionCookie } from "../../cookies/index.mjs";
5
4
  import { getAwaitableValue } from "../../context/helpers.mjs";
6
5
  import { OAUTH_CALLBACK_ERROR_CODES, missingEmailLogMessage } from "../../oauth2/errors.mjs";
6
+ import { getOAuthCallbackPath } from "../../oauth2/utils.mjs";
7
+ import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
7
8
  import { generateIdTokenNonce, generateState } from "../../oauth2/state.mjs";
8
- import { signInWithOAuthIdentity } from "../../oauth2/sign-in-with-oauth-identity.mjs";
9
9
  import { createEmailVerificationToken } from "./email-verification.mjs";
10
10
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
11
11
  import { additionalAuthorizationParamsSchema, supportsIdTokenSignIn, verifyProviderIdToken } from "@better-auth/core/oauth2";
@@ -25,7 +25,6 @@ const socialSignInBodySchema = z.object({
25
25
  accessToken: z.string().meta({ description: "Access token from the provider" }).optional(),
26
26
  refreshToken: z.string().meta({ description: "Refresh token from the provider" }).optional(),
27
27
  expiresAt: z.number().meta({ description: "Expiry date of the token" }).optional(),
28
- scopes: z.array(z.string()).meta({ description: "The scopes granted alongside the id token" }).optional(),
29
28
  user: z.object({
30
29
  name: z.object({
31
30
  firstName: z.string().optional(),
@@ -101,7 +100,7 @@ const signInSocial = () => createAuthEndpoint("/sign-in/social", {
101
100
  c.context.logger.error(missingEmailLogMessage(c.body.provider, { source: "id_token" }), { provider: c.body.provider });
102
101
  throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.USER_EMAIL_NOT_FOUND);
103
102
  }
104
- const data = await signInWithOAuthIdentity(c, {
103
+ const data = await handleOAuthUserInfo(c, {
105
104
  userInfo: {
106
105
  ...userInfo.user,
107
106
  email: userInfo.user.email,
@@ -110,12 +109,10 @@ const signInSocial = () => createAuthEndpoint("/sign-in/social", {
110
109
  image: userInfo.user.image,
111
110
  emailVerified: userInfo.user.emailVerified || false
112
111
  },
113
- providerId: provider.id,
114
- accountId: String(userInfo.user.id),
115
- tokens: {
116
- idToken: token,
117
- accessToken: c.body.idToken.accessToken,
118
- refreshToken: c.body.idToken.refreshToken
112
+ account: {
113
+ providerId: provider.id,
114
+ accountId: String(userInfo.user.id),
115
+ accessToken: c.body.idToken.accessToken
119
116
  },
120
117
  callbackURL: c.body.callbackURL,
121
118
  disableSignUp: provider.disableImplicitSignUp && !c.body.requestSignUp || provider.disableSignUp,
@@ -142,25 +139,20 @@ const signInSocial = () => createAuthEndpoint("/sign-in/social", {
142
139
  user: parseUserOutput(c.context.options, data.data.user)
143
140
  });
144
141
  }
145
- const state = generateRandomString(32);
146
- const codeVerifier = generateRandomString(128);
147
142
  const idTokenNonce = generateIdTokenNonce(provider);
148
- const { url, requestedScopes } = await provider.createAuthorizationURL({
143
+ const { codeVerifier, state } = await generateState(c, {
144
+ additionalData: c.body.additionalData,
145
+ idTokenNonce
146
+ });
147
+ const url = await provider.createAuthorizationURL({
149
148
  state,
150
149
  codeVerifier,
151
150
  idTokenNonce,
152
- redirectURI: `${c.context.baseURL}${provider.callbackPath}`,
151
+ redirectURI: `${c.context.baseURL}${getOAuthCallbackPath(provider)}`,
153
152
  scopes: c.body.scopes,
154
153
  loginHint: c.body.loginHint,
155
154
  additionalParams: c.body.additionalParams
156
155
  });
157
- await generateState(c, {
158
- additionalData: c.body.additionalData,
159
- requestedScopes,
160
- state,
161
- codeVerifier,
162
- idTokenNonce
163
- });
164
156
  if (!c.body.disableRedirect) c.setHeader("Location", url.toString());
165
157
  return c.json({
166
158
  url: url.toString(),
@@ -24,7 +24,7 @@ declare function getAccountCookie(c: GenericEndpointContext): Promise<{
24
24
  idToken?: string | null | undefined;
25
25
  accessTokenExpiresAt?: Date | null | undefined;
26
26
  refreshTokenExpiresAt?: Date | null | undefined;
27
- grantedScopes?: string[] | null | undefined;
27
+ scope?: string | null | undefined;
28
28
  password?: string | null | undefined;
29
29
  } | null>;
30
30
  //#endregion
@@ -87,40 +87,7 @@ function matchType(columnDataType, fieldType, dbType) {
87
87
  function normalize(type) {
88
88
  return type.toLowerCase().split("(")[0].trim();
89
89
  }
90
- if (fieldType === "string[]" || fieldType === "number[]") {
91
- const normalized = normalize(columnDataType);
92
- if (map[dbType].json.map((t) => t.toLowerCase()).includes(normalized)) return true;
93
- if (dbType === "postgres") {
94
- const element = columnDataType.toLowerCase().replace(/^_/, "").replace(/\[\]$/, "").trim();
95
- return (fieldType === "string[]" ? new Set([
96
- "text",
97
- "varchar",
98
- "character varying",
99
- "char",
100
- "character",
101
- "bpchar",
102
- "name",
103
- "uuid",
104
- "citext"
105
- ]) : new Set([
106
- "int",
107
- "int2",
108
- "int4",
109
- "int8",
110
- "integer",
111
- "smallint",
112
- "bigint",
113
- "numeric",
114
- "decimal",
115
- "real",
116
- "float4",
117
- "float8",
118
- "double precision",
119
- "money"
120
- ])).has(element);
121
- }
122
- return false;
123
- }
90
+ if (fieldType === "string[]" || fieldType === "number[]") return columnDataType.toLowerCase().includes("json");
124
91
  const types = map[dbType];
125
92
  return (Array.isArray(fieldType) ? types["string"].map((t) => t.toLowerCase()) : types[fieldType].map((t) => t.toLowerCase())).includes(normalize(columnDataType));
126
93
  }
@@ -89,6 +89,26 @@ const createInternalAdapter = (adapter, ctx) => {
89
89
  }
90
90
  }
91
91
  return {
92
+ createOAuthUser: async (user, account) => {
93
+ return runWithTransaction(adapter, async () => {
94
+ const createdUser = await createWithHooks({
95
+ createdAt: /* @__PURE__ */ new Date(),
96
+ updatedAt: /* @__PURE__ */ new Date(),
97
+ ...user,
98
+ email: user.email?.toLowerCase()
99
+ }, "user", void 0);
100
+ if (!createdUser) throw new APIError("BAD_REQUEST", { message: "Failed to create user" });
101
+ return {
102
+ user: createdUser,
103
+ account: await createWithHooks({
104
+ ...account,
105
+ userId: createdUser.id,
106
+ createdAt: /* @__PURE__ */ new Date(),
107
+ updatedAt: /* @__PURE__ */ new Date()
108
+ }, "account", void 0)
109
+ };
110
+ });
111
+ },
92
112
  createUser: async (user, source) => {
93
113
  const data = {
94
114
  createdAt: /* @__PURE__ */ new Date(),
@@ -37,7 +37,7 @@ declare function parseAccountInput(options: BetterAuthOptions, account: Partial<
37
37
  idToken?: string | null | undefined;
38
38
  accessTokenExpiresAt?: Date | null | undefined;
39
39
  refreshTokenExpiresAt?: Date | null | undefined;
40
- grantedScopes?: string[] | null | undefined;
40
+ scope?: string | null | undefined;
41
41
  password?: string | null | undefined;
42
42
  }>>;
43
43
  declare function parseSessionInput(options: BetterAuthOptions, session: Partial<Session$1>, action?: "create" | "update"): Partial<Partial<{
@@ -1,7 +1,5 @@
1
1
  import { GenerateStateOptions, generateIdTokenNonce, generateState, parseState } from "./state.mjs";
2
- import { PersistOAuthAccountMode, PersistOAuthAccountParams, persistOAuthAccount } from "./persist-account.mjs";
3
- import { OAuthLinkPolicy, ResolveOAuthUserError, ResolveOAuthUserParams, ResolvedOAuthUser, applyUpdateUserInfoOnLink, canLinkImplicitly, resolveOAuthUser } from "./resolve-account.mjs";
4
- import { signInWithOAuthIdentity } from "./sign-in-with-oauth-identity.mjs";
5
- import { decryptOAuthToken, setTokenUtil } from "./token-encryption.mjs";
2
+ import { applyUpdateUserInfoOnLink, handleOAuthUserInfo } from "./link-account.mjs";
3
+ import { decryptOAuthToken, getOAuthCallbackPath, setTokenUtil } from "./utils.mjs";
6
4
  export * from "@better-auth/core/oauth2";
7
- export { GenerateStateOptions, OAuthLinkPolicy, PersistOAuthAccountMode, PersistOAuthAccountParams, ResolveOAuthUserError, ResolveOAuthUserParams, ResolvedOAuthUser, applyUpdateUserInfoOnLink, canLinkImplicitly, decryptOAuthToken, generateIdTokenNonce, generateState, parseState, persistOAuthAccount, resolveOAuthUser, setTokenUtil, signInWithOAuthIdentity };
5
+ export { GenerateStateOptions, applyUpdateUserInfoOnLink, decryptOAuthToken, generateIdTokenNonce, generateState, getOAuthCallbackPath, handleOAuthUserInfo, parseState, setTokenUtil };
@@ -1,7 +1,5 @@
1
- import { decryptOAuthToken, setTokenUtil } from "./token-encryption.mjs";
2
- import { persistOAuthAccount } from "./persist-account.mjs";
3
- import { applyUpdateUserInfoOnLink, canLinkImplicitly, resolveOAuthUser } from "./resolve-account.mjs";
1
+ import { decryptOAuthToken, getOAuthCallbackPath, setTokenUtil } from "./utils.mjs";
2
+ import { applyUpdateUserInfoOnLink, handleOAuthUserInfo } from "./link-account.mjs";
4
3
  import { generateIdTokenNonce, generateState, parseState } from "./state.mjs";
5
- import { signInWithOAuthIdentity } from "./sign-in-with-oauth-identity.mjs";
6
4
  export * from "@better-auth/core/oauth2";
7
- export { applyUpdateUserInfoOnLink, canLinkImplicitly, decryptOAuthToken, generateIdTokenNonce, generateState, parseState, persistOAuthAccount, resolveOAuthUser, setTokenUtil, signInWithOAuthIdentity };
5
+ export { applyUpdateUserInfoOnLink, decryptOAuthToken, generateIdTokenNonce, generateState, getOAuthCallbackPath, handleOAuthUserInfo, parseState, setTokenUtil };
@@ -0,0 +1,74 @@
1
+ import { Account, User } from "../types/models.mjs";
2
+ import { GenericEndpointContext, UserProvisioningSource } from "@better-auth/core";
3
+
4
+ //#region src/oauth2/link-account.d.ts
5
+ declare function handleOAuthUserInfo(c: GenericEndpointContext, opts: {
6
+ userInfo: Omit<User, "createdAt" | "updatedAt">;
7
+ account: Omit<Account, "id" | "userId" | "createdAt" | "updatedAt">;
8
+ callbackURL?: string | undefined;
9
+ disableSignUp?: boolean | undefined;
10
+ overrideUserInfo?: boolean | undefined;
11
+ isTrustedProvider?: boolean | undefined;
12
+ trustProviderByName?: boolean | undefined;
13
+ source?: UserProvisioningSource | undefined;
14
+ }): Promise<{
15
+ error: string;
16
+ data: null;
17
+ isRegister?: undefined;
18
+ } | {
19
+ error: string;
20
+ data: null;
21
+ isRegister: boolean;
22
+ } | {
23
+ data: {
24
+ session: {
25
+ id: string;
26
+ createdAt: Date;
27
+ updatedAt: Date;
28
+ userId: string;
29
+ expiresAt: Date;
30
+ token: string;
31
+ ipAddress?: string | null | undefined;
32
+ userAgent?: string | null | undefined;
33
+ };
34
+ user: {
35
+ id: string;
36
+ createdAt: Date;
37
+ updatedAt: Date;
38
+ email: string;
39
+ emailVerified: boolean;
40
+ name: string;
41
+ image?: string | null | undefined;
42
+ };
43
+ };
44
+ error: null;
45
+ isRegister: boolean;
46
+ }>;
47
+ /**
48
+ * Provider profile a freshly linked account may copy onto the local user.
49
+ * `id` is the provider's account id (never the local user id), and `email`/
50
+ * `emailVerified` are identity anchors; all three are stripped before the
51
+ * remaining fields are written.
52
+ */
53
+ type LinkedProviderProfile = {
54
+ id: string | number;
55
+ name?: string | undefined;
56
+ email?: string | null | undefined;
57
+ emailVerified?: boolean | undefined;
58
+ image?: string | null | undefined;
59
+ };
60
+ /**
61
+ * Apply the `account.accountLinking.updateUserInfoOnLink` policy: when enabled,
62
+ * copy the freshly linked provider's profile onto the local user, matching the
63
+ * field set persisted on sign-up. The local `email` and `emailVerified` are
64
+ * never changed, so a link can't rebind the account's identity, and
65
+ * `updateUser` drops `undefined` fields, so a provider that omits one leaves
66
+ * the existing column intact.
67
+ *
68
+ * Returns the updated user so a caller that issues a session can seed the
69
+ * cookie cache with the fresh row. Returns `undefined` when the policy is
70
+ * disabled or the update fails: a failed profile sync must not abort the link.
71
+ */
72
+ declare function applyUpdateUserInfoOnLink(c: GenericEndpointContext, userId: string, userInfo: LinkedProviderProfile): Promise<User | undefined>;
73
+ //#endregion
74
+ export { applyUpdateUserInfoOnLink, handleOAuthUserInfo };
@@ -0,0 +1,222 @@
1
+ import { isAPIError } from "../utils/is-api-error.mjs";
2
+ import { setAccountCookie } from "../cookies/session-store.mjs";
3
+ import { assertValidUserInfo } from "../utils/validate-user-info.mjs";
4
+ import { OAUTH_CALLBACK_ERROR_CODES, redirectOnError } from "./errors.mjs";
5
+ import { setTokenUtil } from "./utils.mjs";
6
+ import { createEmailVerificationToken } from "../api/routes/email-verification.mjs";
7
+ import { runWithTransaction } from "@better-auth/core/context";
8
+ import { isDevelopment, logger } from "@better-auth/core/env";
9
+ //#region src/oauth2/link-account.ts
10
+ async function handleOAuthUserInfo(c, opts) {
11
+ const { userInfo, account, callbackURL, disableSignUp, overrideUserInfo } = opts;
12
+ const source = opts.source ?? {
13
+ method: "oauth",
14
+ oauth: { providerId: account.providerId }
15
+ };
16
+ const dbUser = await c.context.internalAdapter.findOAuthUser(userInfo.email.toLowerCase(), account.accountId, account.providerId).catch((e) => {
17
+ logger.error("Better auth was unable to query your database.\nError: ", e);
18
+ redirectOnError(c, c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`, "internal_server_error");
19
+ });
20
+ let user = dbUser?.user;
21
+ const isRegister = !user;
22
+ if (dbUser) {
23
+ const linkedAccount = dbUser.linkedAccount ?? dbUser.accounts.find((acc) => acc.providerId === account.providerId && acc.accountId === account.accountId);
24
+ if (!linkedAccount) {
25
+ const accountLinking = c.context.options.account?.accountLinking;
26
+ const isTrustedProvider = opts.isTrustedProvider || opts.trustProviderByName !== false && c.context.trustedProviders.includes(account.providerId);
27
+ const requireLocalEmailVerified = accountLinking?.requireLocalEmailVerified ?? true;
28
+ if (!isTrustedProvider && !userInfo.emailVerified || requireLocalEmailVerified && !dbUser.user.emailVerified || accountLinking?.enabled === false || accountLinking?.disableImplicitLinking === true) {
29
+ if (isDevelopment()) 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.`);
30
+ return {
31
+ error: "account not linked",
32
+ data: null
33
+ };
34
+ }
35
+ try {
36
+ const { id: _providerAccountId, ...providerUserInfo } = userInfo;
37
+ await assertValidUserInfo(c, {
38
+ user: {
39
+ ...providerUserInfo,
40
+ id: dbUser.user.id,
41
+ email: userInfo.email.toLowerCase()
42
+ },
43
+ source: {
44
+ ...source,
45
+ action: "link-account"
46
+ }
47
+ });
48
+ await c.context.internalAdapter.linkAccount({
49
+ providerId: account.providerId,
50
+ accountId: userInfo.id.toString(),
51
+ userId: dbUser.user.id,
52
+ accessToken: await setTokenUtil(account.accessToken, c.context),
53
+ refreshToken: await setTokenUtil(account.refreshToken, c.context),
54
+ idToken: account.idToken,
55
+ accessTokenExpiresAt: account.accessTokenExpiresAt,
56
+ refreshTokenExpiresAt: account.refreshTokenExpiresAt,
57
+ scope: account.scope
58
+ });
59
+ } catch (e) {
60
+ if (isAPIError(e)) throw e;
61
+ logger.error("Unable to link account", e);
62
+ return {
63
+ error: "unable to link account",
64
+ data: null
65
+ };
66
+ }
67
+ if (userInfo.emailVerified && !dbUser.user.emailVerified && userInfo.email.toLowerCase() === dbUser.user.email) await c.context.internalAdapter.updateUser(dbUser.user.id, { emailVerified: true });
68
+ user = await applyUpdateUserInfoOnLink(c, dbUser.user.id, userInfo) ?? user;
69
+ } else {
70
+ const { id: _providerAccountId, ...providerUserInfo } = userInfo;
71
+ await assertValidUserInfo(c, {
72
+ user: {
73
+ ...providerUserInfo,
74
+ id: dbUser.user.id,
75
+ email: userInfo.email.toLowerCase()
76
+ },
77
+ source: {
78
+ ...source,
79
+ action: "sign-in"
80
+ }
81
+ });
82
+ /**
83
+ * `scope` intentionally omitted. Updated only via linkSocial.
84
+ *
85
+ * @see {@link Account.scope}
86
+ */
87
+ const freshTokens = c.context.options.account?.updateAccountOnSignIn !== false ? Object.fromEntries(Object.entries({
88
+ idToken: account.idToken,
89
+ accessToken: await setTokenUtil(account.accessToken, c.context),
90
+ refreshToken: await setTokenUtil(account.refreshToken, c.context),
91
+ accessTokenExpiresAt: account.accessTokenExpiresAt,
92
+ refreshTokenExpiresAt: account.refreshTokenExpiresAt
93
+ }).filter(([_, value]) => value !== void 0)) : {};
94
+ if (c.context.options.account?.storeAccountCookie) await setAccountCookie(c, {
95
+ ...linkedAccount,
96
+ ...freshTokens
97
+ });
98
+ if (Object.keys(freshTokens).length > 0) await c.context.internalAdapter.updateAccount(linkedAccount.id, freshTokens);
99
+ if (userInfo.emailVerified && !dbUser.user.emailVerified && userInfo.email.toLowerCase() === dbUser.user.email) await c.context.internalAdapter.updateUser(dbUser.user.id, { emailVerified: true });
100
+ }
101
+ if (overrideUserInfo) {
102
+ const { id: _, ...restUserInfo } = userInfo;
103
+ const updatedUser = await c.context.internalAdapter.updateUser(dbUser.user.id, {
104
+ ...restUserInfo,
105
+ email: userInfo.email.toLowerCase(),
106
+ emailVerified: userInfo.email.toLowerCase() === dbUser.user.email ? dbUser.user.emailVerified || userInfo.emailVerified : userInfo.emailVerified
107
+ });
108
+ if (updatedUser == null) logger.warn("Could not update user info during OAuth sign in; preserving existing user for session.");
109
+ user = updatedUser ?? user;
110
+ }
111
+ } else {
112
+ if (disableSignUp) return {
113
+ error: "signup disabled",
114
+ data: null,
115
+ isRegister: false
116
+ };
117
+ try {
118
+ const { id: _, ...restUserInfo } = userInfo;
119
+ const accountData = {
120
+ accessToken: await setTokenUtil(account.accessToken, c.context),
121
+ refreshToken: await setTokenUtil(account.refreshToken, c.context),
122
+ idToken: account.idToken,
123
+ accessTokenExpiresAt: account.accessTokenExpiresAt,
124
+ refreshTokenExpiresAt: account.refreshTokenExpiresAt,
125
+ scope: account.scope,
126
+ providerId: account.providerId,
127
+ accountId: userInfo.id.toString()
128
+ };
129
+ const { createdUser, createdAccount } = await runWithTransaction(c.context.adapter, async () => {
130
+ const createdUser = await c.context.internalAdapter.createUser({
131
+ ...restUserInfo,
132
+ email: userInfo.email.toLowerCase()
133
+ }, source);
134
+ return {
135
+ createdUser,
136
+ createdAccount: await c.context.internalAdapter.createAccount({
137
+ ...accountData,
138
+ userId: createdUser.id
139
+ })
140
+ };
141
+ });
142
+ user = createdUser;
143
+ if (c.context.options.account?.storeAccountCookie) await setAccountCookie(c, createdAccount);
144
+ } catch (e) {
145
+ if (isAPIError(e)) throw e;
146
+ logger.error("Unable to create OAuth user", e);
147
+ return {
148
+ error: "unable to create user",
149
+ data: null,
150
+ isRegister: false
151
+ };
152
+ }
153
+ }
154
+ if (!user) return {
155
+ error: "unable to create user",
156
+ data: null,
157
+ isRegister: false
158
+ };
159
+ const requireEmailVerification = c.context.socialProviders.find((p) => p.id === account.providerId)?.options?.requireEmailVerification;
160
+ if (isRegister && !user.emailVerified && (c.context.options.emailVerification?.sendOnSignUp ?? requireEmailVerification)) await dispatchVerificationEmail(c, user, callbackURL);
161
+ if (requireEmailVerification && !user.emailVerified) {
162
+ if (!isRegister && c.context.options.emailVerification?.sendOnSignIn) await dispatchVerificationEmail(c, user, callbackURL);
163
+ return {
164
+ error: OAUTH_CALLBACK_ERROR_CODES.EMAIL_NOT_VERIFIED,
165
+ data: null,
166
+ isRegister
167
+ };
168
+ }
169
+ const session = await c.context.internalAdapter.createSession(user.id);
170
+ if (!session) return {
171
+ error: "unable to create session",
172
+ data: null,
173
+ isRegister: false
174
+ };
175
+ return {
176
+ data: {
177
+ session,
178
+ user
179
+ },
180
+ error: null,
181
+ isRegister
182
+ };
183
+ }
184
+ async function dispatchVerificationEmail(c, user, callbackURL) {
185
+ const sendVerificationEmail = c.context.options.emailVerification?.sendVerificationEmail;
186
+ if (!sendVerificationEmail) return;
187
+ try {
188
+ const token = await createEmailVerificationToken(c.context.secret, user.email, void 0, c.context.options.emailVerification?.expiresIn);
189
+ const url = `${c.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(callbackURL || "/")}`;
190
+ await c.context.runInBackgroundOrAwait(sendVerificationEmail({
191
+ user,
192
+ url,
193
+ token
194
+ }, c.request));
195
+ } catch (e) {
196
+ c.context.logger.error("Failed to send OAuth verification email", e);
197
+ }
198
+ }
199
+ /**
200
+ * Apply the `account.accountLinking.updateUserInfoOnLink` policy: when enabled,
201
+ * copy the freshly linked provider's profile onto the local user, matching the
202
+ * field set persisted on sign-up. The local `email` and `emailVerified` are
203
+ * never changed, so a link can't rebind the account's identity, and
204
+ * `updateUser` drops `undefined` fields, so a provider that omits one leaves
205
+ * the existing column intact.
206
+ *
207
+ * Returns the updated user so a caller that issues a session can seed the
208
+ * cookie cache with the fresh row. Returns `undefined` when the policy is
209
+ * disabled or the update fails: a failed profile sync must not abort the link.
210
+ */
211
+ async function applyUpdateUserInfoOnLink(c, userId, userInfo) {
212
+ if (c.context.options.account?.accountLinking?.updateUserInfoOnLink !== true) return;
213
+ const { id: _id, email: _email, emailVerified: _emailVerified, ...profile } = userInfo;
214
+ try {
215
+ return await c.context.internalAdapter.updateUser(userId, profile);
216
+ } catch (e) {
217
+ c.context.logger.warn("Could not update user info on account link", e);
218
+ return;
219
+ }
220
+ }
221
+ //#endregion
222
+ export { applyUpdateUserInfoOnLink, handleOAuthUserInfo };
@@ -14,14 +14,6 @@ declare function generateIdTokenNonce(provider: {
14
14
  /**
15
15
  * Inputs for {@link generateState}. Grouped into one object so call sites read
16
16
  * by name instead of by position.
17
- *
18
- * `requestedScopes` is the effective scope set encoded in the authorization
19
- * URL, persisted into OAuth state so the callback can fall back to the request
20
- * when the provider omits `scope` from its token response (RFC 6749 §5.1).
21
- * Because that set is only known after `createAuthorizationURL` runs, the
22
- * redirect-initiating routes build the URL first, then pass the URL's `state`
23
- * and `codeVerifier` here so state is still written exactly once. Flows that do
24
- * not need the URL first (e.g. SSO) omit both and let fresh values be minted.
25
17
  */
26
18
  interface GenerateStateOptions {
27
19
  /** Link target when this flow links a provider identity to an existing user. */
@@ -31,8 +23,6 @@ interface GenerateStateOptions {
31
23
  } | undefined;
32
24
  /** Extra data to round-trip through state; `false` writes none. */
33
25
  additionalData?: Record<string, any> | false | undefined;
34
- /** The effective scopes encoded in the authorization URL (the §5.1 fallback). */
35
- requestedScopes?: string[] | undefined;
36
26
  /** The `state` nonce already used to build the authorization URL. Minted when omitted. */
37
27
  state?: string | undefined;
38
28
  /** The PKCE `codeVerifier` already used to build the authorization URL. Minted when omitted. */
@@ -58,7 +48,6 @@ declare function parseState(c: GenericEndpointContext): Promise<{
58
48
  } | undefined;
59
49
  requestSignUp?: boolean | undefined;
60
50
  idTokenNonce?: string | undefined;
61
- requestedScopes?: string[] | undefined;
62
51
  serverContext?: Record<string, unknown> | undefined;
63
52
  }>;
64
53
  //#endregion
@@ -30,12 +30,11 @@ async function generateState(c, options) {
30
30
  serverContext,
31
31
  expiresAt: Date.now() + 600 * 1e3,
32
32
  requestSignUp: c.body?.requestSignUp,
33
- requestedScopes: options?.requestedScopes,
34
33
  idTokenNonce: options?.idTokenNonce
35
34
  };
36
35
  await setOAuthState(stateData);
37
36
  try {
38
- return generateGenericState(c, stateData, { oauthState: options?.state });
37
+ return generateGenericState(c, stateData);
39
38
  } catch (error) {
40
39
  c.context.logger.error("Failed to create verification", error);
41
40
  throw new APIError("INTERNAL_SERVER_ERROR", {
@@ -0,0 +1,11 @@
1
+ import { AuthContext, LiteralString } from "@better-auth/core";
2
+
3
+ //#region src/oauth2/utils.d.ts
4
+ declare function decryptOAuthToken(token: string, ctx: AuthContext): string | Promise<string>;
5
+ declare function setTokenUtil(token: string | null | undefined, ctx: AuthContext): string | Promise<string> | null | undefined;
6
+ declare function getOAuthCallbackPath(provider: {
7
+ id: LiteralString;
8
+ callbackPath?: string | undefined;
9
+ }): string;
10
+ //#endregion
11
+ export { decryptOAuthToken, getOAuthCallbackPath, setTokenUtil };
@@ -1,5 +1,5 @@
1
1
  import { symmetricDecrypt, symmetricEncrypt } from "../crypto/index.mjs";
2
- //#region src/oauth2/token-encryption.ts
2
+ //#region src/oauth2/utils.ts
3
3
  /**
4
4
  * Check if a string looks like encrypted data
5
5
  */
@@ -25,5 +25,9 @@ function setTokenUtil(token, ctx) {
25
25
  });
26
26
  return token;
27
27
  }
28
+ function getOAuthCallbackPath(provider) {
29
+ if (!provider.callbackPath) return `/callback/${provider.id}`;
30
+ return provider.callbackPath.startsWith("/") ? provider.callbackPath : `/${provider.callbackPath}`;
31
+ }
28
32
  //#endregion
29
- export { decryptOAuthToken, setTokenUtil };
33
+ export { decryptOAuthToken, getOAuthCallbackPath, setTokenUtil };
package/dist/package.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.7.0-beta.7";
2
+ var version = "1.7.0-beta.9";
3
3
  //#endregion
4
4
  export { version };
@@ -10,7 +10,7 @@ import { PatreonOptions, patreon } from "./providers/patreon.mjs";
10
10
  import { SlackOptions, slack } from "./providers/slack.mjs";
11
11
  import { AuthContext } from "@better-auth/core";
12
12
  import * as _better_auth_core_oauth20 from "@better-auth/core/oauth2";
13
- import { UpstreamProvider } from "@better-auth/core/oauth2";
13
+ import { OAuthProvider } from "@better-auth/core/oauth2";
14
14
  import * as _better_auth_core_utils_error_codes0 from "@better-auth/core/utils/error-codes";
15
15
 
16
16
  //#region src/plugins/generic-oauth/index.d.ts
@@ -38,7 +38,7 @@ declare const genericOAuth: <const ID extends string>(options: GenericOAuthOptio
38
38
  version: string;
39
39
  init: (ctx: AuthContext) => Promise<{
40
40
  context: {
41
- socialProviders: UpstreamProvider<Record<string, any>, Partial<_better_auth_core_oauth20.ProviderOptions<any>>>[];
41
+ socialProviders: OAuthProvider<Record<string, any>, Partial<_better_auth_core_oauth20.ProviderOptions<any>>>[];
42
42
  };
43
43
  }>;
44
44
  options: GenericOAuthOptions<ID>;
@@ -122,7 +122,6 @@ const genericOAuth = (options) => {
122
122
  const provider = {
123
123
  id: c.providerId,
124
124
  name: c.name ?? c.providerId,
125
- callbackPath: `/callback/${c.providerId}`,
126
125
  issuer,
127
126
  idToken: idTokenConfig,
128
127
  requiresIdTokenNonce: idTokenConfig !== void 0 && c.disableIdTokenNonceBinding !== true,
@@ -152,13 +152,13 @@ const oauthPopupStart = createAuthEndpoint("/oauth-popup/start", {
152
152
  popupOrigin,
153
153
  popupNonce: c.query.popupNonce ?? ""
154
154
  }), c.context.secret, marker.attributes);
155
- ({url} = await provider.createAuthorizationURL({
155
+ url = await provider.createAuthorizationURL({
156
156
  state,
157
157
  codeVerifier,
158
158
  idTokenNonce,
159
159
  redirectURI: `${c.context.baseURL}/callback/${provider.id}`,
160
160
  scopes: c.query.scopes ? c.query.scopes.split(",") : void 0
161
- }));
161
+ });
162
162
  } catch (error) {
163
163
  c.context.logger.error("OAuth popup failed to start", error);
164
164
  return fail("popup_sign_in_failed", "Failed to start the OAuth flow.");