@better-auth/core 1.7.0-beta.4 → 1.7.0-beta.6

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 (170) hide show
  1. package/dist/api/index.d.mts +47 -4
  2. package/dist/api/index.mjs +40 -1
  3. package/dist/context/global.mjs +1 -1
  4. package/dist/context/transaction.d.mts +7 -4
  5. package/dist/context/transaction.mjs +6 -3
  6. package/dist/db/adapter/factory.mjs +57 -31
  7. package/dist/db/adapter/index.d.mts +54 -10
  8. package/dist/db/adapter/types.d.mts +1 -1
  9. package/dist/db/get-tables.mjs +3 -3
  10. package/dist/db/schema/account.d.mts +1 -1
  11. package/dist/db/schema/account.mjs +1 -1
  12. package/dist/db/type.d.mts +12 -7
  13. package/dist/env/env-impl.mjs +1 -1
  14. package/dist/error/codes.d.mts +5 -0
  15. package/dist/error/codes.mjs +5 -0
  16. package/dist/index.d.mts +2 -2
  17. package/dist/instrumentation/tracer.mjs +1 -1
  18. package/dist/oauth2/create-authorization-url.d.mts +4 -1
  19. package/dist/oauth2/create-authorization-url.mjs +5 -2
  20. package/dist/oauth2/dpop.d.mts +142 -0
  21. package/dist/oauth2/dpop.mjs +246 -0
  22. package/dist/oauth2/index.d.mts +6 -3
  23. package/dist/oauth2/index.mjs +5 -2
  24. package/dist/oauth2/oauth-provider.d.mts +128 -9
  25. package/dist/oauth2/refresh-access-token.mjs +1 -1
  26. package/dist/oauth2/scopes.d.mts +76 -0
  27. package/dist/oauth2/scopes.mjs +96 -0
  28. package/dist/oauth2/utils.mjs +2 -1
  29. package/dist/oauth2/verify-id-token.d.mts +26 -0
  30. package/dist/oauth2/verify-id-token.mjs +62 -0
  31. package/dist/oauth2/verify.d.mts +88 -15
  32. package/dist/oauth2/verify.mjs +187 -19
  33. package/dist/social-providers/apple.d.mts +14 -2
  34. package/dist/social-providers/apple.mjs +12 -36
  35. package/dist/social-providers/atlassian.d.mts +5 -1
  36. package/dist/social-providers/atlassian.mjs +4 -4
  37. package/dist/social-providers/cognito.d.mts +13 -2
  38. package/dist/social-providers/cognito.mjs +24 -32
  39. package/dist/social-providers/discord.d.mts +5 -1
  40. package/dist/social-providers/discord.mjs +7 -6
  41. package/dist/social-providers/dropbox.d.mts +5 -1
  42. package/dist/social-providers/dropbox.mjs +5 -5
  43. package/dist/social-providers/facebook.d.mts +21 -2
  44. package/dist/social-providers/facebook.mjs +46 -22
  45. package/dist/social-providers/figma.d.mts +5 -1
  46. package/dist/social-providers/figma.mjs +5 -5
  47. package/dist/social-providers/github.d.mts +5 -1
  48. package/dist/social-providers/github.mjs +4 -4
  49. package/dist/social-providers/gitlab.d.mts +5 -1
  50. package/dist/social-providers/gitlab.mjs +6 -6
  51. package/dist/social-providers/google.d.mts +29 -3
  52. package/dist/social-providers/google.mjs +24 -30
  53. package/dist/social-providers/huggingface.d.mts +5 -1
  54. package/dist/social-providers/huggingface.mjs +8 -8
  55. package/dist/social-providers/index.d.mts +222 -42
  56. package/dist/social-providers/kakao.d.mts +5 -1
  57. package/dist/social-providers/kakao.mjs +8 -8
  58. package/dist/social-providers/kick.d.mts +5 -1
  59. package/dist/social-providers/kick.mjs +4 -4
  60. package/dist/social-providers/line.d.mts +8 -2
  61. package/dist/social-providers/line.mjs +12 -14
  62. package/dist/social-providers/linear.d.mts +5 -1
  63. package/dist/social-providers/linear.mjs +4 -4
  64. package/dist/social-providers/linkedin.d.mts +5 -1
  65. package/dist/social-providers/linkedin.mjs +10 -10
  66. package/dist/social-providers/microsoft-entra-id.d.mts +41 -6
  67. package/dist/social-providers/microsoft-entra-id.mjs +40 -36
  68. package/dist/social-providers/naver.d.mts +5 -1
  69. package/dist/social-providers/naver.mjs +4 -4
  70. package/dist/social-providers/notion.d.mts +5 -1
  71. package/dist/social-providers/notion.mjs +4 -4
  72. package/dist/social-providers/paybin.d.mts +5 -1
  73. package/dist/social-providers/paybin.mjs +10 -10
  74. package/dist/social-providers/paypal.d.mts +5 -2
  75. package/dist/social-providers/paypal.mjs +8 -13
  76. package/dist/social-providers/polar.d.mts +5 -1
  77. package/dist/social-providers/polar.mjs +8 -8
  78. package/dist/social-providers/railway.d.mts +5 -1
  79. package/dist/social-providers/railway.mjs +9 -9
  80. package/dist/social-providers/reddit.d.mts +5 -1
  81. package/dist/social-providers/reddit.mjs +9 -8
  82. package/dist/social-providers/roblox.d.mts +5 -1
  83. package/dist/social-providers/roblox.mjs +5 -5
  84. package/dist/social-providers/salesforce.d.mts +5 -1
  85. package/dist/social-providers/salesforce.mjs +8 -8
  86. package/dist/social-providers/slack.d.mts +5 -1
  87. package/dist/social-providers/slack.mjs +9 -9
  88. package/dist/social-providers/spotify.d.mts +5 -1
  89. package/dist/social-providers/spotify.mjs +5 -5
  90. package/dist/social-providers/tiktok.d.mts +5 -1
  91. package/dist/social-providers/tiktok.mjs +9 -5
  92. package/dist/social-providers/twitch.d.mts +5 -1
  93. package/dist/social-providers/twitch.mjs +4 -4
  94. package/dist/social-providers/twitter.d.mts +6 -4
  95. package/dist/social-providers/twitter.mjs +9 -9
  96. package/dist/social-providers/vercel.d.mts +5 -1
  97. package/dist/social-providers/vercel.mjs +4 -7
  98. package/dist/social-providers/vk.d.mts +5 -1
  99. package/dist/social-providers/vk.mjs +5 -5
  100. package/dist/social-providers/wechat.d.mts +5 -1
  101. package/dist/social-providers/wechat.mjs +10 -6
  102. package/dist/social-providers/zoom.d.mts +6 -1
  103. package/dist/social-providers/zoom.mjs +15 -9
  104. package/dist/types/context.d.mts +27 -8
  105. package/dist/types/index.d.mts +1 -1
  106. package/dist/types/init-options.d.mts +137 -6
  107. package/dist/types/plugin-client.d.mts +12 -2
  108. package/dist/utils/host.mjs +4 -0
  109. package/dist/utils/url.mjs +4 -3
  110. package/package.json +7 -7
  111. package/src/api/index.ts +82 -0
  112. package/src/context/transaction.ts +45 -12
  113. package/src/db/adapter/factory.ts +127 -64
  114. package/src/db/adapter/index.ts +54 -9
  115. package/src/db/adapter/types.ts +1 -0
  116. package/src/db/get-tables.ts +8 -3
  117. package/src/db/schema/account.ts +14 -2
  118. package/src/db/type.ts +12 -7
  119. package/src/env/env-impl.ts +1 -2
  120. package/src/error/codes.ts +5 -0
  121. package/src/oauth2/create-authorization-url.ts +2 -2
  122. package/src/oauth2/dpop.ts +568 -0
  123. package/src/oauth2/index.ts +61 -2
  124. package/src/oauth2/oauth-provider.ts +140 -10
  125. package/src/oauth2/refresh-access-token.ts +2 -2
  126. package/src/oauth2/scopes.ts +118 -0
  127. package/src/oauth2/utils.ts +2 -5
  128. package/src/oauth2/verify-id-token.ts +111 -0
  129. package/src/oauth2/verify.ts +372 -58
  130. package/src/social-providers/apple.ts +24 -61
  131. package/src/social-providers/atlassian.ts +12 -8
  132. package/src/social-providers/cognito.ts +25 -47
  133. package/src/social-providers/discord.ts +19 -8
  134. package/src/social-providers/dropbox.ts +13 -7
  135. package/src/social-providers/facebook.ts +97 -51
  136. package/src/social-providers/figma.ts +13 -9
  137. package/src/social-providers/github.ts +12 -8
  138. package/src/social-providers/gitlab.ts +14 -8
  139. package/src/social-providers/google.ts +66 -47
  140. package/src/social-providers/huggingface.ts +12 -8
  141. package/src/social-providers/kakao.ts +16 -8
  142. package/src/social-providers/kick.ts +12 -7
  143. package/src/social-providers/line.ts +37 -37
  144. package/src/social-providers/linear.ts +12 -6
  145. package/src/social-providers/linkedin.ts +14 -10
  146. package/src/social-providers/microsoft-entra-id.ts +103 -59
  147. package/src/social-providers/naver.ts +12 -6
  148. package/src/social-providers/notion.ts +12 -6
  149. package/src/social-providers/paybin.ts +14 -11
  150. package/src/social-providers/paypal.ts +6 -25
  151. package/src/social-providers/polar.ts +12 -8
  152. package/src/social-providers/railway.ts +13 -9
  153. package/src/social-providers/reddit.ts +25 -10
  154. package/src/social-providers/roblox.ts +18 -7
  155. package/src/social-providers/salesforce.ts +12 -8
  156. package/src/social-providers/slack.ts +18 -9
  157. package/src/social-providers/spotify.ts +13 -7
  158. package/src/social-providers/tiktok.ts +13 -7
  159. package/src/social-providers/twitch.ts +12 -8
  160. package/src/social-providers/twitter.ts +17 -8
  161. package/src/social-providers/vercel.ts +16 -10
  162. package/src/social-providers/vk.ts +13 -7
  163. package/src/social-providers/wechat.ts +28 -9
  164. package/src/social-providers/zoom.ts +19 -6
  165. package/src/types/context.ts +26 -8
  166. package/src/types/index.ts +7 -0
  167. package/src/types/init-options.ts +159 -8
  168. package/src/types/plugin-client.ts +16 -2
  169. package/src/utils/host.ts +15 -0
  170. package/src/utils/url.ts +10 -4
@@ -1,16 +1,30 @@
1
1
  import { base64 } from "@better-auth/utils/base64";
2
2
  import { betterFetch } from "@better-fetch/fetch";
3
- import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
3
+ import { decodeJwt, importJWK } from "jose";
4
4
  import { logger } from "../env";
5
5
  import { APIError, BetterAuthError } from "../error";
6
- import type { OAuthProvider, ProviderOptions } from "../oauth2";
6
+ import type {
7
+ ClientAssertionGetter,
8
+ ProviderOptions,
9
+ TokenEndpointAuth,
10
+ UpstreamProvider,
11
+ } from "../oauth2";
7
12
  import {
8
13
  createAuthorizationURL,
9
14
  getPrimaryClientId,
10
15
  refreshAccessToken,
16
+ resolveRequestedScopes,
11
17
  validateAuthorizationCode,
12
18
  } from "../oauth2";
13
19
 
20
+ /**
21
+ * Microsoft's fixed tenant id for personal (consumer) Microsoft accounts. Every
22
+ * personal-account token carries it as the `tid` claim, so it distinguishes the
23
+ * consumer account class from work/school tenants.
24
+ * @see https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference
25
+ */
26
+ const MICROSOFT_CONSUMER_TENANT_ID = "9188040d-6c67-4c5b-b112-36a304b66dad";
27
+
14
28
  /**
15
29
  * @see [Microsoft Identity Platform - Optional claims reference](https://learn.microsoft.com/en-us/entra/identity-platform/optional-claims-reference)
16
30
  */
@@ -122,33 +136,67 @@ export interface MicrosoftOptions
122
136
  * The tenant ID of the Microsoft account
123
137
  * @default "common"
124
138
  */
125
- tenantId?: string | undefined;
139
+ tenantId?: string;
126
140
  /**
127
141
  * The authentication authority URL. Use the default "https://login.microsoftonline.com" for standard Entra ID or "https://<tenant-id>.ciamlogin.com" for CIAM scenarios.
128
142
  * @default "https://login.microsoftonline.com"
129
143
  */
130
- authority?: string | undefined;
144
+ authority?: string;
145
+ /**
146
+ * Function that returns a JWT client assertion for token endpoint authentication.
147
+ *
148
+ * Use this instead of `clientSecret` when your Microsoft Entra ID app is
149
+ * configured for client authentication with assertions (private_key_jwt or
150
+ * workload identity federation).
151
+ */
152
+ clientAssertion?: ClientAssertionGetter;
131
153
  /**
132
154
  * The size of the profile photo
133
155
  * @default 48
134
156
  */
135
- profilePhotoSize?:
136
- | (48 | 64 | 96 | 120 | 240 | 360 | 432 | 504 | 648)
137
- | undefined;
157
+ profilePhotoSize?: 48 | 64 | 96 | 120 | 240 | 360 | 432 | 504 | 648;
138
158
  /**
139
159
  * Disable profile photo
140
160
  */
141
- disableProfilePhoto?: boolean | undefined;
161
+ disableProfilePhoto?: boolean;
142
162
  }
143
163
 
164
+ const MICROSOFT_ENTRA_ID_DEFAULT_SCOPES = [
165
+ "openid",
166
+ "profile",
167
+ "email",
168
+ "User.Read",
169
+ "offline_access",
170
+ ];
171
+
144
172
  export const microsoft = (options: MicrosoftOptions) => {
145
173
  const tenant = options.tenantId || "common";
146
- const authority = options.authority || "https://login.microsoftonline.com";
174
+ // Trim any trailing slash so endpoint URLs and the issuer comparison below
175
+ // never produce a double slash (e.g. a configured `https://host/` would make
176
+ // the expected issuer `https://host//<tid>/v2.0` and reject every token). A
177
+ // loop avoids a trailing-slash regex, which is a polynomial-ReDoS shape.
178
+ let authority = options.authority || "https://login.microsoftonline.com";
179
+ while (authority.endsWith("/")) {
180
+ authority = authority.slice(0, -1);
181
+ }
147
182
  const authorizationEndpoint = `${authority}/${tenant}/oauth2/v2.0/authorize`;
148
183
  const tokenEndpoint = `${authority}/${tenant}/oauth2/v2.0/token`;
184
+ if (options.clientSecret && options.clientAssertion) {
185
+ throw new BetterAuthError(
186
+ "Microsoft Entra ID clientAssertion cannot be combined with clientSecret",
187
+ );
188
+ }
189
+ const tokenEndpointAuth: TokenEndpointAuth | undefined =
190
+ options.clientAssertion
191
+ ? {
192
+ method: "private_key_jwt",
193
+ getClientAssertion: options.clientAssertion,
194
+ }
195
+ : undefined;
149
196
  return {
150
197
  id: "microsoft",
151
198
  name: "Microsoft EntraID",
199
+ callbackPath: "/callback/microsoft",
152
200
  createAuthorizationURL(data) {
153
201
  // Microsoft Entra supports public clients (SPA / native apps with
154
202
  // PKCE only), so clientSecret is intentionally not required here.
@@ -159,18 +207,18 @@ export const microsoft = (options: MicrosoftOptions) => {
159
207
  );
160
208
  throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
161
209
  }
162
- const scopes = options.disableDefaultScope
163
- ? []
164
- : ["openid", "profile", "email", "User.Read", "offline_access"];
165
- if (options.scope) scopes.push(...options.scope);
166
- if (data.scopes) scopes.push(...data.scopes);
210
+ const requestedScopes = resolveRequestedScopes(
211
+ options,
212
+ MICROSOFT_ENTRA_ID_DEFAULT_SCOPES,
213
+ data.scopes,
214
+ );
167
215
  return createAuthorizationURL({
168
216
  id: "microsoft",
169
217
  options,
170
218
  authorizationEndpoint,
171
219
  state: data.state,
172
220
  codeVerifier: data.codeVerifier,
173
- scopes,
221
+ scopes: requestedScopes,
174
222
  redirectURI: data.redirectURI,
175
223
  prompt: options.prompt,
176
224
  loginHint: data.loginHint,
@@ -184,57 +232,52 @@ export const microsoft = (options: MicrosoftOptions) => {
184
232
  redirectURI,
185
233
  options,
186
234
  tokenEndpoint,
235
+ tokenEndpointAuth,
187
236
  });
188
237
  },
189
- async verifyIdToken(token, nonce) {
190
- if (options.disableIdTokenSignIn) {
191
- return false;
192
- }
193
- if (options.verifyIdToken) {
194
- return options.verifyIdToken(token, nonce);
195
- }
196
-
197
- try {
198
- const { kid, alg: jwtAlg } = decodeProtectedHeader(token);
199
- if (!kid || !jwtAlg) return false;
200
-
201
- const publicKey = await getMicrosoftPublicKey(kid, tenant, authority);
202
- const verifyOptions: {
203
- algorithms: [string];
204
- audience: string | string[];
205
- maxTokenAge: string;
206
- issuer?: string;
207
- } = {
208
- algorithms: [jwtAlg],
209
- audience: options.clientId,
210
- maxTokenAge: "1h",
211
- };
212
- /**
213
- * Issuer varies per user's tenant for multi-tenant endpoints, so only validate for specific tenants.
214
- * @see https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols#endpoints
215
- */
238
+ idToken: {
239
+ jwks: (header) => getMicrosoftPublicKey(header.kid!, tenant, authority),
240
+ audience: options.clientId,
241
+ maxTokenAge: "1h",
242
+ /**
243
+ * Issuer varies per tenant for multi-tenant endpoints, so only validate it for
244
+ * specific tenants.
245
+ * @see https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols#endpoints
246
+ */
247
+ issuer:
248
+ tenant !== "common" &&
249
+ tenant !== "organizations" &&
250
+ tenant !== "consumers"
251
+ ? `${authority}/${tenant}/v2.0`
252
+ : undefined,
253
+ /**
254
+ * The multi-tenant endpoints (common/organizations/consumers) skip the
255
+ * issuer check above because the issuer varies per tenant, and the
256
+ * organizations and consumers JWKS sets overlap. Enforce the tenant
257
+ * binding explicitly so a token from a disallowed account class cannot
258
+ * pass: the issuer must name the token's own tenant, and the account
259
+ * class must match the configured restriction.
260
+ * @see https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference
261
+ */
262
+ verifyClaims: (claims) => {
263
+ const tid = claims.tid;
216
264
  if (
217
- tenant !== "common" &&
218
- tenant !== "organizations" &&
219
- tenant !== "consumers"
265
+ typeof tid !== "string" ||
266
+ claims.iss !== `${authority}/${tid}/v2.0`
220
267
  ) {
221
- verifyOptions.issuer = `${authority}/${tenant}/v2.0`;
268
+ return false;
222
269
  }
223
- const { payload: jwtClaims } = await jwtVerify(
224
- token,
225
- publicKey,
226
- verifyOptions,
227
- );
228
-
229
- if (nonce && jwtClaims.nonce !== nonce) {
270
+ if (
271
+ tenant === "organizations" &&
272
+ tid === MICROSOFT_CONSUMER_TENANT_ID
273
+ ) {
274
+ return false;
275
+ }
276
+ if (tenant === "consumers" && tid !== MICROSOFT_CONSUMER_TENANT_ID) {
230
277
  return false;
231
278
  }
232
-
233
279
  return true;
234
- } catch (error) {
235
- logger.error("Failed to verify ID token:", error);
236
- return false;
237
- }
280
+ },
238
281
  },
239
282
  async getUserInfo(token) {
240
283
  if (options.getUserInfo) {
@@ -314,10 +357,11 @@ export const microsoft = (options: MicrosoftOptions) => {
314
357
  scope: scopes.join(" "), // Include the scopes in request to microsoft
315
358
  },
316
359
  tokenEndpoint,
360
+ tokenEndpointAuth,
317
361
  });
318
362
  },
319
363
  options,
320
- } satisfies OAuthProvider;
364
+ } satisfies UpstreamProvider;
321
365
  };
322
366
 
323
367
  export const getMicrosoftPublicKey = async (
@@ -1,8 +1,9 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
- import type { OAuthProvider, ProviderOptions } from "../oauth2";
2
+ import type { ProviderOptions, UpstreamProvider } from "../oauth2";
3
3
  import {
4
4
  createAuthorizationURL,
5
5
  refreshAccessToken,
6
+ resolveRequestedScopes,
6
7
  validateAuthorizationCode,
7
8
  } from "../oauth2";
8
9
 
@@ -39,20 +40,25 @@ export interface NaverOptions extends ProviderOptions<NaverProfile> {
39
40
  clientId: string;
40
41
  }
41
42
 
43
+ const NAVER_DEFAULT_SCOPES = ["profile", "email"];
44
+
42
45
  export const naver = (options: NaverOptions) => {
43
46
  const tokenEndpoint = "https://nid.naver.com/oauth2.0/token";
44
47
  return {
45
48
  id: "naver",
46
49
  name: "Naver",
50
+ callbackPath: "/callback/naver",
47
51
  createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
48
- const _scopes = options.disableDefaultScope ? [] : ["profile", "email"];
49
- if (options.scope) _scopes.push(...options.scope);
50
- if (scopes) _scopes.push(...scopes);
52
+ const requestedScopes = resolveRequestedScopes(
53
+ options,
54
+ NAVER_DEFAULT_SCOPES,
55
+ scopes,
56
+ );
51
57
  return createAuthorizationURL({
52
58
  id: "naver",
53
59
  options,
54
60
  authorizationEndpoint: "https://nid.naver.com/oauth2.0/authorize",
55
- scopes: _scopes,
61
+ scopes: requestedScopes,
56
62
  state,
57
63
  redirectURI,
58
64
  additionalParams,
@@ -110,5 +116,5 @@ export const naver = (options: NaverOptions) => {
110
116
  };
111
117
  },
112
118
  options,
113
- } satisfies OAuthProvider<NaverProfile>;
119
+ } satisfies UpstreamProvider<NaverProfile>;
114
120
  };
@@ -1,8 +1,9 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
- import type { OAuthProvider, ProviderOptions } from "../oauth2";
2
+ import type { ProviderOptions, UpstreamProvider } from "../oauth2";
3
3
  import {
4
4
  createAuthorizationURL,
5
5
  refreshAccessToken,
6
+ resolveRequestedScopes,
6
7
  validateAuthorizationCode,
7
8
  } from "../oauth2";
8
9
 
@@ -23,11 +24,14 @@ export interface NotionOptions extends ProviderOptions<NotionProfile> {
23
24
  clientId: string;
24
25
  }
25
26
 
27
+ const NOTION_DEFAULT_SCOPES: string[] = [];
28
+
26
29
  export const notion = (options: NotionOptions) => {
27
30
  const tokenEndpoint = "https://api.notion.com/v1/oauth/token";
28
31
  return {
29
32
  id: "notion",
30
33
  name: "Notion",
34
+ callbackPath: "/callback/notion",
31
35
  createAuthorizationURL({
32
36
  state,
33
37
  scopes,
@@ -35,14 +39,16 @@ export const notion = (options: NotionOptions) => {
35
39
  redirectURI,
36
40
  additionalParams,
37
41
  }) {
38
- const _scopes: string[] = options.disableDefaultScope ? [] : [];
39
- if (options.scope) _scopes.push(...options.scope);
40
- if (scopes) _scopes.push(...scopes);
42
+ const requestedScopes = resolveRequestedScopes(
43
+ options,
44
+ NOTION_DEFAULT_SCOPES,
45
+ scopes,
46
+ );
41
47
  return createAuthorizationURL({
42
48
  id: "notion",
43
49
  options,
44
50
  authorizationEndpoint: "https://api.notion.com/v1/oauth/authorize",
45
- scopes: _scopes,
51
+ scopes: requestedScopes,
46
52
  state,
47
53
  redirectURI,
48
54
  loginHint,
@@ -111,5 +117,5 @@ export const notion = (options: NotionOptions) => {
111
117
  };
112
118
  },
113
119
  options,
114
- } satisfies OAuthProvider<NotionProfile>;
120
+ } satisfies UpstreamProvider<NotionProfile>;
115
121
  };
@@ -1,10 +1,11 @@
1
1
  import { decodeJwt } from "jose";
2
2
  import { logger } from "../env";
3
3
  import { BetterAuthError } from "../error";
4
- import type { OAuthProvider, ProviderOptions } from "../oauth2";
4
+ import type { ProviderOptions, UpstreamProvider } from "../oauth2";
5
5
  import {
6
6
  createAuthorizationURL,
7
7
  refreshAccessToken,
8
+ resolveRequestedScopes,
8
9
  validateAuthorizationCode,
9
10
  } from "../oauth2";
10
11
 
@@ -28,6 +29,8 @@ export interface PaybinOptions extends ProviderOptions<PaybinProfile> {
28
29
  issuer?: string | undefined;
29
30
  }
30
31
 
32
+ const PAYBIN_DEFAULT_SCOPES = ["openid", "email", "profile"];
33
+
31
34
  export const paybin = (options: PaybinOptions) => {
32
35
  const issuer = options.issuer || "https://idp.paybin.io";
33
36
  const authorizationEndpoint = `${issuer}/oauth2/authorize`;
@@ -36,7 +39,8 @@ export const paybin = (options: PaybinOptions) => {
36
39
  return {
37
40
  id: "paybin",
38
41
  name: "Paybin",
39
- async createAuthorizationURL({
42
+ callbackPath: "/callback/paybin",
43
+ createAuthorizationURL({
40
44
  state,
41
45
  scopes,
42
46
  codeVerifier,
@@ -53,16 +57,16 @@ export const paybin = (options: PaybinOptions) => {
53
57
  if (!codeVerifier) {
54
58
  throw new BetterAuthError("codeVerifier is required for Paybin");
55
59
  }
56
- const _scopes = options.disableDefaultScope
57
- ? []
58
- : ["openid", "email", "profile"];
59
- if (options.scope) _scopes.push(...options.scope);
60
- if (scopes) _scopes.push(...scopes);
61
- const url = await createAuthorizationURL({
60
+ const requestedScopes = resolveRequestedScopes(
61
+ options,
62
+ PAYBIN_DEFAULT_SCOPES,
63
+ scopes,
64
+ );
65
+ return createAuthorizationURL({
62
66
  id: "paybin",
63
67
  options,
64
68
  authorizationEndpoint,
65
- scopes: _scopes,
69
+ scopes: requestedScopes,
66
70
  state,
67
71
  codeVerifier,
68
72
  redirectURI,
@@ -70,7 +74,6 @@ export const paybin = (options: PaybinOptions) => {
70
74
  loginHint,
71
75
  additionalParams,
72
76
  });
73
- return url;
74
77
  },
75
78
  validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
76
79
  return validateAuthorizationCode({
@@ -116,5 +119,5 @@ export const paybin = (options: PaybinOptions) => {
116
119
  };
117
120
  },
118
121
  options,
119
- } satisfies OAuthProvider<PaybinProfile>;
122
+ } satisfies UpstreamProvider<PaybinProfile>;
120
123
  };
@@ -1,9 +1,8 @@
1
1
  import { base64 } from "@better-auth/utils/base64";
2
2
  import { betterFetch } from "@better-fetch/fetch";
3
- import { decodeJwt } from "jose";
4
3
  import { logger } from "../env";
5
4
  import { BetterAuthError } from "../error";
6
- import type { OAuthProvider, ProviderOptions } from "../oauth2";
5
+ import type { ProviderOptions, UpstreamProvider } from "../oauth2";
7
6
  import { createAuthorizationURL } from "../oauth2";
8
7
 
9
8
  export interface PayPalProfile {
@@ -78,7 +77,8 @@ export const paypal = (options: PayPalOptions) => {
78
77
  return {
79
78
  id: "paypal",
80
79
  name: "PayPal",
81
- async createAuthorizationURL({
80
+ callbackPath: "/callback/paypal",
81
+ createAuthorizationURL({
82
82
  state,
83
83
  codeVerifier,
84
84
  redirectURI,
@@ -97,20 +97,17 @@ export const paypal = (options: PayPalOptions) => {
97
97
  * We don't pass any scopes to avoid "invalid scope" errors
98
98
  **/
99
99
 
100
- const _scopes: string[] = [];
101
-
102
- const url = await createAuthorizationURL({
100
+ return createAuthorizationURL({
103
101
  id: "paypal",
104
102
  options,
105
103
  authorizationEndpoint,
106
- scopes: _scopes,
104
+ scopes: [],
107
105
  state,
108
106
  codeVerifier,
109
107
  redirectURI,
110
108
  prompt: options.prompt,
111
109
  additionalParams,
112
110
  });
113
- return url;
114
111
  },
115
112
 
116
113
  validateAuthorizationCode: async ({ code, redirectURI }) => {
@@ -200,22 +197,6 @@ export const paypal = (options: PayPalOptions) => {
200
197
  }
201
198
  },
202
199
 
203
- async verifyIdToken(token, nonce) {
204
- if (options.disableIdTokenSignIn) {
205
- return false;
206
- }
207
- if (options.verifyIdToken) {
208
- return options.verifyIdToken(token, nonce);
209
- }
210
- try {
211
- const payload = decodeJwt(token);
212
- return !!payload.sub;
213
- } catch (error) {
214
- logger.error("Failed to verify PayPal ID token:", error);
215
- return false;
216
- }
217
- },
218
-
219
200
  async getUserInfo(token) {
220
201
  if (options.getUserInfo) {
221
202
  return options.getUserInfo(token);
@@ -265,5 +246,5 @@ export const paypal = (options: PayPalOptions) => {
265
246
  },
266
247
 
267
248
  options,
268
- } satisfies OAuthProvider<PayPalProfile>;
249
+ } satisfies UpstreamProvider<PayPalProfile>;
269
250
  };
@@ -1,8 +1,9 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
- import type { OAuthProvider, ProviderOptions } from "../oauth2";
2
+ import type { ProviderOptions, UpstreamProvider } from "../oauth2";
3
3
  import {
4
4
  createAuthorizationURL,
5
5
  refreshAccessToken,
6
+ resolveRequestedScopes,
6
7
  validateAuthorizationCode,
7
8
  } from "../oauth2";
8
9
 
@@ -32,11 +33,14 @@ export interface PolarProfile {
32
33
 
33
34
  export interface PolarOptions extends ProviderOptions<PolarProfile> {}
34
35
 
36
+ const POLAR_DEFAULT_SCOPES = ["openid", "profile", "email"];
37
+
35
38
  export const polar = (options: PolarOptions) => {
36
39
  const tokenEndpoint = "https://api.polar.sh/v1/oauth2/token";
37
40
  return {
38
41
  id: "polar",
39
42
  name: "Polar",
43
+ callbackPath: "/callback/polar",
40
44
  createAuthorizationURL({
41
45
  state,
42
46
  scopes,
@@ -44,16 +48,16 @@ export const polar = (options: PolarOptions) => {
44
48
  redirectURI,
45
49
  additionalParams,
46
50
  }) {
47
- const _scopes = options.disableDefaultScope
48
- ? []
49
- : ["openid", "profile", "email"];
50
- if (options.scope) _scopes.push(...options.scope);
51
- if (scopes) _scopes.push(...scopes);
51
+ const requestedScopes = resolveRequestedScopes(
52
+ options,
53
+ POLAR_DEFAULT_SCOPES,
54
+ scopes,
55
+ );
52
56
  return createAuthorizationURL({
53
57
  id: "polar",
54
58
  options,
55
59
  authorizationEndpoint: "https://polar.sh/oauth2/authorize",
56
- scopes: _scopes,
60
+ scopes: requestedScopes,
57
61
  state,
58
62
  codeVerifier,
59
63
  redirectURI,
@@ -114,5 +118,5 @@ export const polar = (options: PolarOptions) => {
114
118
  };
115
119
  },
116
120
  options,
117
- } satisfies OAuthProvider<PolarProfile>;
121
+ } satisfies UpstreamProvider<PolarProfile>;
118
122
  };
@@ -1,8 +1,9 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
- import type { OAuthProvider, ProviderOptions } from "../oauth2";
2
+ import type { ProviderOptions, UpstreamProvider } from "../oauth2";
3
3
  import {
4
4
  createAuthorizationURL,
5
5
  refreshAccessToken,
6
+ resolveRequestedScopes,
6
7
  validateAuthorizationCode,
7
8
  } from "../oauth2";
8
9
 
@@ -25,27 +26,30 @@ export interface RailwayOptions extends ProviderOptions<RailwayProfile> {
25
26
  clientId: string;
26
27
  }
27
28
 
29
+ const RAILWAY_DEFAULT_SCOPES = ["openid", "email", "profile"];
30
+
28
31
  export const railway = (options: RailwayOptions) => {
29
32
  return {
30
33
  id: "railway",
31
34
  name: "Railway",
32
- createAuthorizationURL({
35
+ callbackPath: "/callback/railway",
36
+ async createAuthorizationURL({
33
37
  state,
34
38
  scopes,
35
39
  codeVerifier,
36
40
  redirectURI,
37
41
  additionalParams,
38
42
  }) {
39
- const _scopes = options.disableDefaultScope
40
- ? []
41
- : ["openid", "email", "profile"];
42
- if (options.scope) _scopes.push(...options.scope);
43
- if (scopes) _scopes.push(...scopes);
43
+ const requestedScopes = resolveRequestedScopes(
44
+ options,
45
+ RAILWAY_DEFAULT_SCOPES,
46
+ scopes,
47
+ );
44
48
  return createAuthorizationURL({
45
49
  id: "railway",
46
50
  options,
47
51
  authorizationEndpoint,
48
- scopes: _scopes,
52
+ scopes: requestedScopes,
49
53
  state,
50
54
  codeVerifier,
51
55
  redirectURI,
@@ -103,5 +107,5 @@ export const railway = (options: RailwayOptions) => {
103
107
  };
104
108
  },
105
109
  options,
106
- } satisfies OAuthProvider<RailwayProfile>;
110
+ } satisfies UpstreamProvider<RailwayProfile>;
107
111
  };
@@ -1,10 +1,11 @@
1
1
  import { base64 } from "@better-auth/utils/base64";
2
2
  import { betterFetch } from "@better-fetch/fetch";
3
- import type { OAuthProvider, ProviderOptions } from "../oauth2";
3
+ import type { ProviderOptions, UpstreamProvider } from "../oauth2";
4
4
  import {
5
5
  createAuthorizationURL,
6
6
  getOAuth2Tokens,
7
7
  refreshAccessToken,
8
+ resolveRequestedScopes,
8
9
  } from "../oauth2";
9
10
 
10
11
  export interface RedditProfile {
@@ -21,19 +22,29 @@ export interface RedditOptions extends ProviderOptions<RedditProfile> {
21
22
  duration?: string | undefined;
22
23
  }
23
24
 
25
+ const REDDIT_DEFAULT_SCOPES = ["identity"];
26
+
24
27
  export const reddit = (options: RedditOptions) => {
25
28
  return {
26
29
  id: "reddit",
27
30
  name: "Reddit",
28
- createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
29
- const _scopes = options.disableDefaultScope ? [] : ["identity"];
30
- if (options.scope) _scopes.push(...options.scope);
31
- if (scopes) _scopes.push(...scopes);
31
+ callbackPath: "/callback/reddit",
32
+ async createAuthorizationURL({
33
+ state,
34
+ scopes,
35
+ redirectURI,
36
+ additionalParams,
37
+ }) {
38
+ const requestedScopes = resolveRequestedScopes(
39
+ options,
40
+ REDDIT_DEFAULT_SCOPES,
41
+ scopes,
42
+ );
32
43
  return createAuthorizationURL({
33
44
  id: "reddit",
34
45
  options,
35
46
  authorizationEndpoint: "https://www.reddit.com/api/v1/authorize",
36
- scopes: _scopes,
47
+ scopes: requestedScopes,
37
48
  state,
38
49
  redirectURI,
39
50
  duration: options.duration,
@@ -105,19 +116,23 @@ export const reddit = (options: RedditOptions) => {
105
116
  }
106
117
 
107
118
  const userMap = await options.mapProfileToUser?.(profile);
108
-
119
+ // Reddit's identity scope does not return an email. Synthesize a stable,
120
+ // non-routable placeholder (RFC 2606 `.invalid`) keyed to the user's
121
+ // Reddit id rather than the routable `reddit.com`, which could collide
122
+ // with a real address. Left unverified; `mapProfileToUser` can override.
123
+ const email = userMap?.email || `${profile.id}@reddit.invalid`;
109
124
  return {
110
125
  user: {
111
126
  id: profile.id,
112
127
  name: profile.name,
113
- email: profile.oauth_client_id,
114
- emailVerified: profile.has_verified_email,
115
128
  image: profile.icon_img?.split("?")[0]!,
116
129
  ...userMap,
130
+ email,
131
+ emailVerified: userMap?.emailVerified ?? false,
117
132
  },
118
133
  data: profile,
119
134
  };
120
135
  },
121
136
  options,
122
- } satisfies OAuthProvider<RedditProfile>;
137
+ } satisfies UpstreamProvider<RedditProfile>;
123
138
  };