better-auth 1.7.0-beta.6 → 1.7.0-beta.8

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 (40) 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 +86 -66
  4. package/dist/api/routes/callback.mjs +43 -30
  5. package/dist/api/routes/sign-in.d.mts +0 -2
  6. package/dist/api/routes/sign-in.mjs +16 -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/index.d.mts +2 -2
  12. package/dist/index.mjs +2 -2
  13. package/dist/oauth2/errors.mjs +1 -0
  14. package/dist/oauth2/index.d.mts +4 -6
  15. package/dist/oauth2/index.mjs +4 -6
  16. package/dist/oauth2/link-account.d.mts +74 -0
  17. package/dist/oauth2/link-account.mjs +222 -0
  18. package/dist/oauth2/state.d.mts +14 -12
  19. package/dist/oauth2/state.mjs +13 -3
  20. package/dist/oauth2/utils.d.mts +11 -0
  21. package/dist/oauth2/{token-encryption.mjs → utils.mjs} +6 -2
  22. package/dist/package.mjs +1 -1
  23. package/dist/plugins/generic-oauth/index.d.mts +2 -2
  24. package/dist/plugins/generic-oauth/index.mjs +11 -7
  25. package/dist/plugins/generic-oauth/types.d.mts +35 -1
  26. package/dist/plugins/oauth-popup/index.mjs +6 -2
  27. package/dist/plugins/oauth-proxy/index.mjs +6 -20
  28. package/dist/plugins/one-tap/index.mjs +6 -10
  29. package/dist/state.d.mts +5 -8
  30. package/dist/state.mjs +2 -2
  31. package/dist/test-utils/http-test-instance.d.mts +0 -20
  32. package/dist/utils/index.d.mts +1 -1
  33. package/package.json +8 -8
  34. package/dist/oauth2/persist-account.d.mts +0 -80
  35. package/dist/oauth2/persist-account.mjs +0 -84
  36. package/dist/oauth2/resolve-account.d.mts +0 -126
  37. package/dist/oauth2/resolve-account.mjs +0 -128
  38. package/dist/oauth2/sign-in-with-oauth-identity.d.mts +0 -83
  39. package/dist/oauth2/sign-in-with-oauth-identity.mjs +0 -133
  40. package/dist/oauth2/token-encryption.d.mts +0 -7
@@ -100,7 +100,6 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
100
100
  accessToken: zod.ZodOptional<zod.ZodString>;
101
101
  refreshToken: zod.ZodOptional<zod.ZodString>;
102
102
  expiresAt: zod.ZodOptional<zod.ZodNumber>;
103
- scopes: zod.ZodOptional<zod.ZodArray<zod.ZodString>>;
104
103
  user: zod.ZodOptional<zod.ZodObject<{
105
104
  name: zod.ZodOptional<zod.ZodObject<{
106
105
  firstName: zod.ZodOptional<zod.ZodString>;
@@ -129,7 +128,6 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
129
128
  accessToken: zod.ZodOptional<zod.ZodString>;
130
129
  refreshToken: zod.ZodOptional<zod.ZodString>;
131
130
  expiresAt: zod.ZodOptional<zod.ZodNumber>;
132
- scopes: zod.ZodOptional<zod.ZodArray<zod.ZodString>>;
133
131
  user: zod.ZodOptional<zod.ZodObject<{
134
132
  name: zod.ZodOptional<zod.ZodObject<{
135
133
  firstName: zod.ZodOptional<zod.ZodString>;
@@ -1584,7 +1582,6 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
1584
1582
  nonce: zod.ZodOptional<zod.ZodString>;
1585
1583
  accessToken: zod.ZodOptional<zod.ZodString>;
1586
1584
  refreshToken: zod.ZodOptional<zod.ZodString>;
1587
- scopes: zod.ZodOptional<zod.ZodArray<zod.ZodString>>;
1588
1585
  }, zod_v4_core0.$strip>>;
1589
1586
  requestSignUp: zod.ZodOptional<zod.ZodBoolean>;
1590
1587
  scopes: zod.ZodOptional<zod.ZodArray<zod.ZodString>>;
@@ -1712,7 +1709,7 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
1712
1709
  userId: {
1713
1710
  type: string;
1714
1711
  };
1715
- grantedScopes: {
1712
+ scopes: {
1716
1713
  type: string;
1717
1714
  items: {
1718
1715
  type: string;
@@ -1729,7 +1726,7 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
1729
1726
  };
1730
1727
  };
1731
1728
  }, {
1732
- grantedScopes: string[];
1729
+ scopes: string[];
1733
1730
  id: string;
1734
1731
  createdAt: Date;
1735
1732
  updatedAt: Date;
@@ -1869,12 +1866,6 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
1869
1866
  type: string;
1870
1867
  format: string;
1871
1868
  };
1872
- grantedScopes: {
1873
- type: string;
1874
- items: {
1875
- type: string;
1876
- };
1877
- };
1878
1869
  };
1879
1870
  };
1880
1871
  };
@@ -1891,7 +1882,7 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
1891
1882
  refreshToken: string;
1892
1883
  accessTokenExpiresAt: Date | undefined;
1893
1884
  refreshTokenExpiresAt: Date | null | undefined;
1894
- grantedScopes: string[];
1885
+ scope: string | null | undefined;
1895
1886
  idToken: string | null | undefined;
1896
1887
  providerId: string;
1897
1888
  accountId: string;
@@ -1941,7 +1932,7 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
1941
1932
  }, {
1942
1933
  accessToken: string;
1943
1934
  accessTokenExpiresAt: Date | undefined;
1944
- grantedScopes: string[];
1935
+ scopes: string[];
1945
1936
  idToken: string | undefined;
1946
1937
  }>;
1947
1938
  readonly accountInfo: better_call0.StrictEndpoint<"/account-info", {
@@ -2084,7 +2075,6 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
2084
2075
  accessToken: zod.ZodOptional<zod.ZodString>;
2085
2076
  refreshToken: zod.ZodOptional<zod.ZodString>;
2086
2077
  expiresAt: zod.ZodOptional<zod.ZodNumber>;
2087
- scopes: zod.ZodOptional<zod.ZodArray<zod.ZodString>>;
2088
2078
  user: zod.ZodOptional<zod.ZodObject<{
2089
2079
  name: zod.ZodOptional<zod.ZodObject<{
2090
2080
  firstName: zod.ZodOptional<zod.ZodString>;
@@ -2113,7 +2103,6 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
2113
2103
  accessToken: zod.ZodOptional<zod.ZodString>;
2114
2104
  refreshToken: zod.ZodOptional<zod.ZodString>;
2115
2105
  expiresAt: zod.ZodOptional<zod.ZodNumber>;
2116
- scopes: zod.ZodOptional<zod.ZodArray<zod.ZodString>>;
2117
2106
  user: zod.ZodOptional<zod.ZodObject<{
2118
2107
  name: zod.ZodOptional<zod.ZodObject<{
2119
2108
  firstName: zod.ZodOptional<zod.ZodString>;
@@ -3568,7 +3557,6 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
3568
3557
  nonce: zod.ZodOptional<zod.ZodString>;
3569
3558
  accessToken: zod.ZodOptional<zod.ZodString>;
3570
3559
  refreshToken: zod.ZodOptional<zod.ZodString>;
3571
- scopes: zod.ZodOptional<zod.ZodArray<zod.ZodString>>;
3572
3560
  }, zod_v4_core0.$strip>>;
3573
3561
  requestSignUp: zod.ZodOptional<zod.ZodBoolean>;
3574
3562
  scopes: zod.ZodOptional<zod.ZodArray<zod.ZodString>>;
@@ -3696,7 +3684,7 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
3696
3684
  userId: {
3697
3685
  type: string;
3698
3686
  };
3699
- grantedScopes: {
3687
+ scopes: {
3700
3688
  type: string;
3701
3689
  items: {
3702
3690
  type: string;
@@ -3713,7 +3701,7 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
3713
3701
  };
3714
3702
  };
3715
3703
  }, {
3716
- grantedScopes: string[];
3704
+ scopes: string[];
3717
3705
  id: string;
3718
3706
  createdAt: Date;
3719
3707
  updatedAt: Date;
@@ -3853,12 +3841,6 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
3853
3841
  type: string;
3854
3842
  format: string;
3855
3843
  };
3856
- grantedScopes: {
3857
- type: string;
3858
- items: {
3859
- type: string;
3860
- };
3861
- };
3862
3844
  };
3863
3845
  };
3864
3846
  };
@@ -3875,7 +3857,7 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
3875
3857
  refreshToken: string;
3876
3858
  accessTokenExpiresAt: Date | undefined;
3877
3859
  refreshTokenExpiresAt: Date | null | undefined;
3878
- grantedScopes: string[];
3860
+ scope: string | null | undefined;
3879
3861
  idToken: string | null | undefined;
3880
3862
  providerId: string;
3881
3863
  accountId: string;
@@ -3925,7 +3907,7 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
3925
3907
  }, {
3926
3908
  accessToken: string;
3927
3909
  accessTokenExpiresAt: Date | undefined;
3928
- grantedScopes: string[];
3910
+ scopes: string[];
3929
3911
  idToken: string | undefined;
3930
3912
  }>;
3931
3913
  readonly accountInfo: better_call0.StrictEndpoint<"/account-info", {
@@ -62,7 +62,7 @@ declare const listUserAccounts: better_call0.StrictEndpoint<"/list-accounts", {
62
62
  userId: {
63
63
  type: string;
64
64
  };
65
- grantedScopes: {
65
+ scopes: {
66
66
  type: string;
67
67
  items: {
68
68
  type: string;
@@ -79,7 +79,7 @@ declare const listUserAccounts: better_call0.StrictEndpoint<"/list-accounts", {
79
79
  };
80
80
  };
81
81
  }, {
82
- grantedScopes: string[];
82
+ scopes: string[];
83
83
  id: string;
84
84
  createdAt: Date;
85
85
  updatedAt: Date;
@@ -98,7 +98,6 @@ declare const linkSocialAccount: better_call0.StrictEndpoint<"/link-social", {
98
98
  nonce: z.ZodOptional<z.ZodString>;
99
99
  accessToken: z.ZodOptional<z.ZodString>;
100
100
  refreshToken: z.ZodOptional<z.ZodString>;
101
- scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
102
101
  }, z.core.$strip>>;
103
102
  requestSignUp: z.ZodOptional<z.ZodBoolean>;
104
103
  scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -266,7 +265,7 @@ declare const getAccessToken: better_call0.StrictEndpoint<"/get-access-token", {
266
265
  }, {
267
266
  accessToken: string;
268
267
  accessTokenExpiresAt: Date | undefined;
269
- grantedScopes: string[];
268
+ scopes: string[];
270
269
  idToken: string | undefined;
271
270
  }>;
272
271
  declare const refreshToken: better_call0.StrictEndpoint<"/refresh-token", {
@@ -307,12 +306,6 @@ declare const refreshToken: better_call0.StrictEndpoint<"/refresh-token", {
307
306
  type: string;
308
307
  format: string;
309
308
  };
310
- grantedScopes: {
311
- type: string;
312
- items: {
313
- type: string;
314
- };
315
- };
316
309
  };
317
310
  };
318
311
  };
@@ -329,7 +322,7 @@ declare const refreshToken: better_call0.StrictEndpoint<"/refresh-token", {
329
322
  refreshToken: string;
330
323
  accessTokenExpiresAt: Date | undefined;
331
324
  refreshTokenExpiresAt: Date | null | undefined;
332
- grantedScopes: string[];
325
+ scope: string | null | undefined;
333
326
  idToken: string | null | undefined;
334
327
  providerId: string;
335
328
  accountId: string;
@@ -1,20 +1,22 @@
1
1
  import { shouldBindAccountCookieToSessionUser } from "../../context/store-capabilities.mjs";
2
2
  import { parseAccountOutput } from "../../db/schema.mjs";
3
- import { generateRandomString } from "../../crypto/random.mjs";
4
- import { getAccountCookie } from "../../cookies/session-store.mjs";
3
+ import { getAccountCookie, setAccountCookie } from "../../cookies/session-store.mjs";
5
4
  import { getAwaitableValue } from "../../context/helpers.mjs";
6
5
  import { missingEmailLogMessage } from "../../oauth2/errors.mjs";
7
- import { decryptOAuthToken } from "../../oauth2/token-encryption.mjs";
8
- import { persistOAuthAccount } from "../../oauth2/persist-account.mjs";
9
- import { applyUpdateUserInfoOnLink } from "../../oauth2/resolve-account.mjs";
10
- import { generateState } from "../../oauth2/state.mjs";
6
+ import { decryptOAuthToken, getOAuthCallbackPath, setTokenUtil } from "../../oauth2/utils.mjs";
7
+ import { applyUpdateUserInfoOnLink } from "../../oauth2/link-account.mjs";
8
+ import { generateIdTokenNonce, generateState } from "../../oauth2/state.mjs";
11
9
  import { freshSessionMiddleware, getSessionFromCtx, isStateful, sessionMiddleware } from "./session.mjs";
12
10
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
13
- import { additionalAuthorizationParamsSchema, readGrantedScopes, supportsIdTokenSignIn, verifyProviderIdToken } from "@better-auth/core/oauth2";
11
+ import { additionalAuthorizationParamsSchema, supportsIdTokenSignIn, verifyProviderIdToken } from "@better-auth/core/oauth2";
14
12
  import { SocialProviderListEnum } from "@better-auth/core/social-providers";
15
13
  import { createAuthEndpoint } from "@better-auth/core/api";
16
14
  import * as z from "zod";
17
15
  //#region src/api/routes/account.ts
16
+ function parseStoredScopes(scope) {
17
+ if (!scope) return [];
18
+ return scope.split(",").map((s) => s.trim()).filter(Boolean);
19
+ }
18
20
  const listUserAccounts = createAuthEndpoint("/list-accounts", {
19
21
  method: "GET",
20
22
  use: [sessionMiddleware],
@@ -40,7 +42,7 @@ const listUserAccounts = createAuthEndpoint("/list-accounts", {
40
42
  },
41
43
  accountId: { type: "string" },
42
44
  userId: { type: "string" },
43
- grantedScopes: {
45
+ scopes: {
44
46
  type: "array",
45
47
  items: { type: "string" }
46
48
  }
@@ -52,7 +54,7 @@ const listUserAccounts = createAuthEndpoint("/list-accounts", {
52
54
  "updatedAt",
53
55
  "accountId",
54
56
  "userId",
55
- "grantedScopes"
57
+ "scopes"
56
58
  ]
57
59
  }
58
60
  } } }
@@ -62,10 +64,10 @@ const listUserAccounts = createAuthEndpoint("/list-accounts", {
62
64
  const session = c.context.session;
63
65
  const accounts = await c.context.internalAdapter.findAccounts(session.user.id);
64
66
  return c.json(accounts.map((a) => {
65
- const { grantedScopes, ...parsed } = parseAccountOutput(c.context.options, a);
67
+ const { scope, ...parsed } = parseAccountOutput(c.context.options, a);
66
68
  return {
67
69
  ...parsed,
68
- grantedScopes: readGrantedScopes(grantedScopes)
70
+ scopes: parseStoredScopes(scope)
69
71
  };
70
72
  }));
71
73
  });
@@ -79,8 +81,7 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
79
81
  token: z.string(),
80
82
  nonce: z.string().optional(),
81
83
  accessToken: z.string().optional(),
82
- refreshToken: z.string().optional(),
83
- scopes: z.array(z.string()).optional()
84
+ refreshToken: z.string().optional()
84
85
  }).optional(),
85
86
  requestSignUp: z.boolean().optional(),
86
87
  scopes: z.array(z.string()).meta({ description: "Additional scopes to request from the provider" }).optional(),
@@ -158,18 +159,15 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
158
159
  code: "LINKING_DIFFERENT_EMAILS_NOT_ALLOWED"
159
160
  });
160
161
  try {
161
- await persistOAuthAccount(c, {
162
+ await c.context.internalAdapter.createAccount({
162
163
  userId: session.user.id,
163
164
  providerId: provider.id,
164
165
  accountId: linkingUserId,
165
- tokens: {
166
- accessToken: c.body.idToken.accessToken,
167
- refreshToken: c.body.idToken.refreshToken,
168
- idToken: token
169
- },
170
- mode: "link"
166
+ accessToken: c.body.idToken.accessToken,
167
+ idToken: token,
168
+ refreshToken: c.body.idToken.refreshToken
171
169
  });
172
- } catch (_e) {
170
+ } catch {
173
171
  throw APIError.from("EXPECTATION_FAILED", {
174
172
  message: "Account not linked - unable to create account",
175
173
  code: "LINKING_FAILED"
@@ -182,25 +180,23 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
182
180
  redirect: false
183
181
  });
184
182
  }
185
- const stateNonce = generateRandomString(32);
186
- const codeVerifier = generateRandomString(128);
187
- const { url, requestedScopes } = await provider.createAuthorizationURL({
188
- state: stateNonce,
189
- codeVerifier,
190
- redirectURI: `${c.context.baseURL}${provider.callbackPath}`,
191
- scopes: c.body.scopes,
192
- loginHint: c.body.loginHint,
193
- additionalParams: c.body.additionalParams
194
- });
195
- await generateState(c, {
183
+ const idTokenNonce = generateIdTokenNonce(provider);
184
+ const state = await generateState(c, {
196
185
  link: {
197
186
  userId: session.user.id,
198
187
  email: session.user.email
199
188
  },
200
189
  additionalData: c.body.additionalData,
201
- requestedScopes,
202
- state: stateNonce,
203
- codeVerifier
190
+ idTokenNonce
191
+ });
192
+ const url = await provider.createAuthorizationURL({
193
+ state: state.state,
194
+ codeVerifier: state.codeVerifier,
195
+ idTokenNonce,
196
+ redirectURI: `${c.context.baseURL}${getOAuthCallbackPath(provider)}`,
197
+ scopes: c.body.scopes,
198
+ loginHint: c.body.loginHint,
199
+ additionalParams: c.body.additionalParams
204
200
  });
205
201
  if (!c.body.disableRedirect) c.setHeader("Location", url.toString());
206
202
  return c.json({
@@ -273,7 +269,7 @@ async function getValidAccessToken(ctx, { resolvedUserId, providerId, accountId,
273
269
  const provider = await getAwaitableValue(ctx.context.socialProviders, { value: providerId });
274
270
  if (!provider) throw APIError.from("BAD_REQUEST", {
275
271
  message: `Provider ${providerId} is not supported.`,
276
- code: BASE_ERROR_CODES.PROVIDER_NOT_SUPPORTED.code
272
+ code: "PROVIDER_NOT_SUPPORTED"
277
273
  });
278
274
  let account = resolvedAccount;
279
275
  if (!account) {
@@ -291,13 +287,19 @@ async function getValidAccessToken(ctx, { resolvedUserId, providerId, accountId,
291
287
  const accessTokenExpired = account.accessTokenExpiresAt && new Date(account.accessTokenExpiresAt).getTime() - Date.now() < 5e3;
292
288
  if (account.refreshToken && accessTokenExpired && provider.refreshAccessToken) {
293
289
  const refreshToken = await decryptOAuthToken(account.refreshToken, ctx.context);
294
- newTokens = await provider.refreshAccessToken(refreshToken);
295
- await persistOAuthAccount(ctx, {
296
- userId: account.userId,
297
- providerId: account.providerId,
298
- accountId: account.accountId,
299
- tokens: newTokens,
300
- mode: "refresh"
290
+ newTokens = await provider.refreshAccessToken(refreshToken, ctx);
291
+ const updatedData = {
292
+ accessToken: await setTokenUtil(newTokens?.accessToken, ctx.context),
293
+ accessTokenExpiresAt: newTokens?.accessTokenExpiresAt,
294
+ refreshToken: newTokens?.refreshToken ? await setTokenUtil(newTokens.refreshToken, ctx.context) : account.refreshToken,
295
+ refreshTokenExpiresAt: newTokens?.refreshTokenExpiresAt ?? account.refreshTokenExpiresAt,
296
+ idToken: newTokens?.idToken || account.idToken
297
+ };
298
+ let updatedAccount = null;
299
+ if (account.id) updatedAccount = await ctx.context.internalAdapter.updateAccount(account.id, updatedData);
300
+ if (ctx.context.options.account?.storeAccountCookie) await setAccountCookie(ctx, {
301
+ ...account,
302
+ ...updatedAccount ?? updatedData
301
303
  });
302
304
  }
303
305
  const accessTokenExpiresAt = (() => {
@@ -313,12 +315,14 @@ async function getValidAccessToken(ctx, { resolvedUserId, providerId, accountId,
313
315
  return {
314
316
  accessToken: newTokens?.accessToken ?? await decryptOAuthToken(account.accessToken ?? "", ctx.context),
315
317
  accessTokenExpiresAt,
316
- grantedScopes: readGrantedScopes(account.grantedScopes),
318
+ scopes: parseStoredScopes(account.scope),
317
319
  idToken: newTokens?.idToken ?? account.idToken ?? void 0
318
320
  };
319
- } catch (error) {
320
- ctx.context.logger.error("Failed to get a valid access token", error);
321
- throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.FAILED_TO_GET_ACCESS_TOKEN);
321
+ } catch (_error) {
322
+ throw APIError.from("BAD_REQUEST", {
323
+ message: "Failed to get a valid access token",
324
+ code: "FAILED_TO_GET_ACCESS_TOKEN"
325
+ });
322
326
  }
323
327
  }
324
328
  const getAccessToken = createAuthEndpoint("/get-access-token", {
@@ -384,10 +388,6 @@ const refreshToken = createAuthEndpoint("/refresh-token", {
384
388
  refreshTokenExpiresAt: {
385
389
  type: "string",
386
390
  format: "date-time"
387
- },
388
- grantedScopes: {
389
- type: "array",
390
- items: { type: "string" }
391
391
  }
392
392
  }
393
393
  } } }
@@ -401,11 +401,11 @@ const refreshToken = createAuthEndpoint("/refresh-token", {
401
401
  const provider = await getAwaitableValue(ctx.context.socialProviders, { value: providerId });
402
402
  if (!provider) throw APIError.from("BAD_REQUEST", {
403
403
  message: `Provider ${providerId} is not supported.`,
404
- code: BASE_ERROR_CODES.PROVIDER_NOT_SUPPORTED.code
404
+ code: "PROVIDER_NOT_SUPPORTED"
405
405
  });
406
406
  if (!provider.refreshAccessToken) throw APIError.from("BAD_REQUEST", {
407
407
  message: `Provider ${providerId} does not support token refreshing.`,
408
- code: BASE_ERROR_CODES.TOKEN_REFRESH_NOT_SUPPORTED.code
408
+ code: "TOKEN_REFRESH_NOT_SUPPORTED"
409
409
  });
410
410
  let account = void 0;
411
411
  const accountData = await getAccountCookie(ctx);
@@ -417,30 +417,50 @@ const refreshToken = createAuthEndpoint("/refresh-token", {
417
417
  else account = (await ctx.context.internalAdapter.findAccounts(resolvedUserId)).find((acc) => accountId ? acc.accountId === accountId && acc.providerId === providerId : acc.providerId === providerId);
418
418
  if (!account) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
419
419
  const refreshToken = account.refreshToken ?? void 0;
420
- if (!refreshToken) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.REFRESH_TOKEN_NOT_FOUND);
420
+ if (!refreshToken) throw APIError.from("BAD_REQUEST", {
421
+ message: "Refresh token not found",
422
+ code: "REFRESH_TOKEN_NOT_FOUND"
423
+ });
421
424
  try {
422
425
  const decryptedRefreshToken = await decryptOAuthToken(refreshToken, ctx.context);
423
- const tokens = await provider.refreshAccessToken(decryptedRefreshToken);
424
- await persistOAuthAccount(ctx, {
425
- userId: account.userId,
426
- providerId: account.providerId,
427
- accountId: account.accountId,
428
- tokens,
429
- mode: "refresh"
426
+ const tokens = await provider.refreshAccessToken(decryptedRefreshToken, ctx);
427
+ const resolvedRefreshToken = tokens.refreshToken ? await setTokenUtil(tokens.refreshToken, ctx.context) : refreshToken;
428
+ const resolvedRefreshTokenExpiresAt = tokens.refreshTokenExpiresAt ?? account.refreshTokenExpiresAt;
429
+ const updatedTokenData = {
430
+ accessToken: await setTokenUtil(tokens.accessToken, ctx.context),
431
+ refreshToken: resolvedRefreshToken,
432
+ accessTokenExpiresAt: tokens.accessTokenExpiresAt,
433
+ refreshTokenExpiresAt: resolvedRefreshTokenExpiresAt,
434
+ idToken: tokens.idToken || account.idToken
435
+ };
436
+ let updatedAccount = null;
437
+ if (account.id)
438
+ /**
439
+ * `scope` intentionally omitted. Refresh response may be narrower.
440
+ *
441
+ * @see {@link Account.scope}
442
+ */
443
+ updatedAccount = await ctx.context.internalAdapter.updateAccount(account.id, updatedTokenData);
444
+ if (accountData && providerId === accountData.providerId && ctx.context.options.account?.storeAccountCookie) await setAccountCookie(ctx, {
445
+ ...accountData,
446
+ ...updatedAccount ?? updatedTokenData
430
447
  });
448
+ const responseScope = updatedAccount?.scope ?? account.scope;
431
449
  return ctx.json({
432
450
  accessToken: tokens.accessToken,
433
451
  refreshToken: tokens.refreshToken ?? decryptedRefreshToken,
434
452
  accessTokenExpiresAt: tokens.accessTokenExpiresAt,
435
- refreshTokenExpiresAt: tokens.refreshTokenExpiresAt ?? account.refreshTokenExpiresAt,
436
- grantedScopes: readGrantedScopes(account.grantedScopes),
453
+ refreshTokenExpiresAt: resolvedRefreshTokenExpiresAt,
454
+ scope: responseScope,
437
455
  idToken: tokens.idToken || account.idToken,
438
456
  providerId: account.providerId,
439
457
  accountId: account.accountId
440
458
  });
441
- } catch (error) {
442
- ctx.context.logger.error("Failed to refresh access token", error);
443
- throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.FAILED_TO_REFRESH_ACCESS_TOKEN);
459
+ } catch (_error) {
460
+ throw APIError.from("BAD_REQUEST", {
461
+ message: "Failed to refresh access token",
462
+ code: "FAILED_TO_REFRESH_ACCESS_TOKEN"
463
+ });
444
464
  }
445
465
  });
446
466
  const accountInfoQuerySchema = z.optional(z.object({
@@ -1,14 +1,13 @@
1
1
  import { isAPIError } from "../../utils/is-api-error.mjs";
2
- import { generateRandomString } from "../../crypto/random.mjs";
3
2
  import { setSessionCookie } from "../../cookies/index.mjs";
4
3
  import { assertValidUserInfo } from "../../utils/validate-user-info.mjs";
5
4
  import { getAwaitableValue } from "../../context/helpers.mjs";
6
5
  import { OAUTH_CALLBACK_ERROR_CODES, missingEmailLogMessage } from "../../oauth2/errors.mjs";
7
- import { persistOAuthAccount } from "../../oauth2/persist-account.mjs";
8
- import { applyUpdateUserInfoOnLink } from "../../oauth2/resolve-account.mjs";
9
- import { generateState, parseState } from "../../oauth2/state.mjs";
10
- import { signInWithOAuthIdentity } from "../../oauth2/sign-in-with-oauth-identity.mjs";
6
+ import { getOAuthCallbackPath, setTokenUtil } from "../../oauth2/utils.mjs";
7
+ import { applyUpdateUserInfoOnLink, handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
8
+ import { generateIdTokenNonce, generateState, parseState } from "../../oauth2/state.mjs";
11
9
  import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
10
+ import { mergeScopes } from "@better-auth/core/oauth2";
12
11
  import { safeJSONParse } from "@better-auth/core/utils/json";
13
12
  import { createAuthEndpoint } from "@better-auth/core/api";
14
13
  import * as z from "zod";
@@ -58,17 +57,13 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
58
57
  if (state === void 0 && code) {
59
58
  const provider = await getAwaitableValue(c.context.socialProviders, { value: c.params.id });
60
59
  if (provider?.allowIdpInitiated) {
61
- const state = generateRandomString(32);
62
- const codeVerifier = generateRandomString(128);
63
- const { url: authUrl, requestedScopes } = await provider.createAuthorizationURL({
64
- state,
60
+ const idTokenNonce = generateIdTokenNonce(provider);
61
+ const { state: freshState, codeVerifier } = await generateState(c, { idTokenNonce });
62
+ const authUrl = await provider.createAuthorizationURL({
63
+ state: freshState,
65
64
  codeVerifier,
66
- redirectURI: `${c.context.baseURL}${provider.callbackPath}`
67
- });
68
- await generateState(c, {
69
- requestedScopes,
70
- state,
71
- codeVerifier
65
+ idTokenNonce,
66
+ redirectURI: `${c.context.baseURL}${getOAuthCallbackPath(provider)}`
72
67
  });
73
68
  throw c.redirect(authUrl.toString());
74
69
  }
@@ -78,7 +73,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
78
73
  const url = `${defaultErrorURL}${defaultErrorURL.includes("?") ? "&" : "?"}error=state_not_found`;
79
74
  throw c.redirect(url);
80
75
  }
81
- const { codeVerifier, callbackURL, link, errorURL, newUserURL, requestSignUp, requestedScopes } = await parseState(c);
76
+ const { codeVerifier, callbackURL, link, errorURL, newUserURL, requestSignUp, idTokenNonce } = await parseState(c);
82
77
  function redirectOnError(error, description) {
83
78
  const baseURL = errorURL ?? defaultErrorURL;
84
79
  const params = new URLSearchParams({ error });
@@ -103,13 +98,17 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
103
98
  });
104
99
  throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.ISSUER_MISMATCH);
105
100
  }
101
+ if (provider.requiresIdTokenNonce && !idTokenNonce) {
102
+ c.context.logger.error("OAuth id_token nonce binding required but no expected nonce was found in state", { providerId: provider.id });
103
+ throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.NONCE_BINDING_MISSING);
104
+ }
106
105
  let tokens;
107
106
  try {
108
107
  tokens = await provider.validateAuthorizationCode({
109
108
  code,
110
109
  codeVerifier,
111
110
  deviceId: device_id,
112
- redirectURI: `${c.context.baseURL}${provider.callbackPath}`
111
+ redirectURI: `${c.context.baseURL}${getOAuthCallbackPath(provider)}`
113
112
  });
114
113
  } catch (e) {
115
114
  c.context.logger.error("", e);
@@ -119,6 +118,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
119
118
  const parsedUserData = userData ? safeJSONParse(userData) : null;
120
119
  const providerResult = await provider.getUserInfo({
121
120
  ...tokens,
121
+ ...idTokenNonce ? { expectedIdTokenNonce: idTokenNonce } : {},
122
122
  user: parsedUserData ?? void 0
123
123
  });
124
124
  if (!providerResult?.user || providerResult.user.id === void 0 || providerResult.user.id === null || providerResult.user.id === "") {
@@ -158,15 +158,26 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
158
158
  }
159
159
  if (userInfo.email?.toLowerCase() !== link.email.toLowerCase() && c.context.options.account?.accountLinking?.allowDifferentEmails !== true) return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.EMAIL_DOES_NOT_MATCH);
160
160
  const existingAccount = await c.context.internalAdapter.findAccountByProviderId(providerAccountId, provider.id);
161
- if (existingAccount && existingAccount.userId.toString() !== link.userId.toString()) return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.ACCOUNT_ALREADY_LINKED_TO_DIFFERENT_USER);
162
- if (!await persistOAuthAccount(c, {
161
+ if (existingAccount) {
162
+ if (existingAccount.userId.toString() !== link.userId.toString()) return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.ACCOUNT_ALREADY_LINKED_TO_DIFFERENT_USER);
163
+ const mergedScope = mergeScopes(existingAccount.scope, tokens.scopes);
164
+ const updateData = Object.fromEntries(Object.entries({
165
+ accessToken: await setTokenUtil(tokens.accessToken, c.context),
166
+ refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
167
+ idToken: tokens.idToken,
168
+ accessTokenExpiresAt: tokens.accessTokenExpiresAt,
169
+ refreshTokenExpiresAt: tokens.refreshTokenExpiresAt,
170
+ scope: mergedScope || void 0
171
+ }).filter(([_, value]) => value !== void 0));
172
+ await c.context.internalAdapter.updateAccount(existingAccount.id, updateData);
173
+ } else if (!await c.context.internalAdapter.createAccount({
163
174
  userId: link.userId,
164
175
  providerId: provider.id,
165
176
  accountId: providerAccountId,
166
- tokens,
167
- requestedScopes,
168
- mode: "link",
169
- grantAuthority: provider.grantAuthority
177
+ ...tokens,
178
+ accessToken: await setTokenUtil(tokens.accessToken, c.context),
179
+ refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
180
+ scope: tokens.scopes?.join(",")
170
181
  })) return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_LINK_ACCOUNT);
171
182
  await applyUpdateUserInfoOnLink(c, link.userId, userInfo);
172
183
  let toRedirectTo;
@@ -181,19 +192,22 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
181
192
  c.context.logger.error(missingEmailLogMessage(provider.id));
182
193
  return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.EMAIL_NOT_FOUND);
183
194
  }
195
+ const accountData = {
196
+ providerId: provider.id,
197
+ accountId: providerAccountId,
198
+ ...tokens,
199
+ scope: tokens.scopes?.join(",")
200
+ };
184
201
  let result;
185
202
  try {
186
- result = await signInWithOAuthIdentity(c, {
203
+ result = await handleOAuthUserInfo(c, {
187
204
  userInfo: {
188
205
  ...userInfo,
189
206
  id: providerAccountId,
190
207
  email: userInfo.email,
191
208
  name: userInfo.name || ""
192
209
  },
193
- providerId: provider.id,
194
- accountId: providerAccountId,
195
- tokens,
196
- requestedScopes,
210
+ account: accountData,
197
211
  callbackURL,
198
212
  disableSignUp: provider.disableImplicitSignUp && !requestSignUp || provider.options?.disableSignUp,
199
213
  overrideUserInfo: provider.options?.overrideUserInfoOnSignIn,
@@ -203,8 +217,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
203
217
  providerId: provider.id,
204
218
  profile: providerResult.data
205
219
  }
206
- },
207
- grantAuthority: provider.grantAuthority
220
+ }
208
221
  });
209
222
  } catch (e) {
210
223
  if (isAPIError(e) && e.body?.code) redirectOnError(e.body.code, e.body.message);
@@ -16,7 +16,6 @@ declare const socialSignInBodySchema: z.ZodObject<{
16
16
  accessToken: z.ZodOptional<z.ZodString>;
17
17
  refreshToken: z.ZodOptional<z.ZodString>;
18
18
  expiresAt: z.ZodOptional<z.ZodNumber>;
19
- scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
20
19
  user: z.ZodOptional<z.ZodObject<{
21
20
  name: z.ZodOptional<z.ZodObject<{
22
21
  firstName: z.ZodOptional<z.ZodString>;
@@ -46,7 +45,6 @@ declare const signInSocial: <O extends BetterAuthOptions>() => better_call0.Stri
46
45
  accessToken: z.ZodOptional<z.ZodString>;
47
46
  refreshToken: z.ZodOptional<z.ZodString>;
48
47
  expiresAt: z.ZodOptional<z.ZodNumber>;
49
- scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
50
48
  user: z.ZodOptional<z.ZodObject<{
51
49
  name: z.ZodOptional<z.ZodObject<{
52
50
  firstName: z.ZodOptional<z.ZodString>;