@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,12 +1,13 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
- import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
2
+ import { decodeJwt, importJWK } from "jose";
3
3
  import { logger } from "../env";
4
4
  import { APIError, BetterAuthError } from "../error";
5
- import type { OAuthProvider, ProviderOptions } from "../oauth2";
5
+ import type { ProviderOptions, UpstreamProvider } from "../oauth2";
6
6
  import {
7
7
  createAuthorizationURL,
8
8
  getPrimaryClientId,
9
9
  refreshAccessToken,
10
+ resolveRequestedScopes,
10
11
  validateAuthorizationCode,
11
12
  } from "../oauth2";
12
13
 
@@ -57,6 +58,8 @@ export interface CognitoOptions extends ProviderOptions<CognitoProfile> {
57
58
  identityProvider?: string | undefined;
58
59
  }
59
60
 
61
+ const COGNITO_DEFAULT_SCOPES = ["openid", "profile", "email"];
62
+
60
63
  export const cognito = (options: CognitoOptions) => {
61
64
  if (!options.domain || !options.region || !options.userPoolId) {
62
65
  logger.error(
@@ -73,6 +76,7 @@ export const cognito = (options: CognitoOptions) => {
73
76
  return {
74
77
  id: "cognito",
75
78
  name: "Cognito",
79
+ callbackPath: "/callback/cognito",
76
80
  async createAuthorizationURL({
77
81
  state,
78
82
  scopes,
@@ -93,19 +97,19 @@ export const cognito = (options: CognitoOptions) => {
93
97
  );
94
98
  throw new BetterAuthError("CLIENT_SECRET_REQUIRED");
95
99
  }
96
- const _scopes = options.disableDefaultScope
97
- ? []
98
- : ["openid", "profile", "email"];
99
- if (options.scope) _scopes.push(...options.scope);
100
- if (scopes) _scopes.push(...scopes);
100
+ const requestedScopes = resolveRequestedScopes(
101
+ options,
102
+ COGNITO_DEFAULT_SCOPES,
103
+ scopes,
104
+ );
101
105
 
102
- const url = await createAuthorizationURL({
106
+ const { url } = await createAuthorizationURL({
103
107
  id: "cognito",
104
108
  options: {
105
109
  ...options,
106
110
  },
107
111
  authorizationEndpoint,
108
- scopes: _scopes,
112
+ scopes: requestedScopes,
109
113
  state,
110
114
  codeVerifier,
111
115
  redirectURI,
@@ -126,9 +130,12 @@ export const cognito = (options: CognitoOptions) => {
126
130
  // Manually append the scope with proper encoding to the URL
127
131
  const urlString = url.toString();
128
132
  const separator = urlString.includes("?") ? "&" : "?";
129
- return new URL(`${urlString}${separator}scope=${encodedScope}`);
133
+ return {
134
+ url: new URL(`${urlString}${separator}scope=${encodedScope}`),
135
+ requestedScopes,
136
+ };
130
137
  }
131
- return url;
138
+ return { url, requestedScopes };
132
139
  },
133
140
 
134
141
  validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
@@ -155,41 +162,12 @@ export const cognito = (options: CognitoOptions) => {
155
162
  });
156
163
  },
157
164
 
158
- async verifyIdToken(token, nonce) {
159
- if (options.disableIdTokenSignIn) {
160
- return false;
161
- }
162
- if (options.verifyIdToken) {
163
- return options.verifyIdToken(token, nonce);
164
- }
165
-
166
- try {
167
- const decodedHeader = decodeProtectedHeader(token);
168
- const { kid, alg: jwtAlg } = decodedHeader;
169
- if (!kid || !jwtAlg) return false;
170
-
171
- const publicKey = await getCognitoPublicKey(
172
- kid,
173
- options.region,
174
- options.userPoolId,
175
- );
176
- const expectedIssuer = `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`;
177
-
178
- const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
179
- algorithms: [jwtAlg],
180
- issuer: expectedIssuer,
181
- audience: options.clientId,
182
- maxTokenAge: "1h",
183
- });
184
-
185
- if (nonce && jwtClaims.nonce !== nonce) {
186
- return false;
187
- }
188
- return true;
189
- } catch (error) {
190
- logger.error("Failed to verify ID token:", error);
191
- return false;
192
- }
165
+ idToken: {
166
+ jwks: (header) =>
167
+ getCognitoPublicKey(header.kid!, options.region, options.userPoolId),
168
+ issuer: `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`,
169
+ audience: options.clientId,
170
+ maxTokenAge: "1h",
193
171
  },
194
172
 
195
173
  async getUserInfo(token) {
@@ -265,7 +243,7 @@ export const cognito = (options: CognitoOptions) => {
265
243
  },
266
244
 
267
245
  options,
268
- } satisfies OAuthProvider<CognitoProfile>;
246
+ } satisfies UpstreamProvider<CognitoProfile>;
269
247
  };
270
248
 
271
249
  export const getCognitoPublicKey = 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
  export interface DiscordProfile extends Record<string, any> {
@@ -83,21 +84,31 @@ export interface DiscordOptions extends ProviderOptions<DiscordProfile> {
83
84
  permissions?: number | undefined;
84
85
  }
85
86
 
87
+ const DISCORD_DEFAULT_SCOPES = ["identify", "email"];
88
+
86
89
  export const discord = (options: DiscordOptions) => {
87
90
  const tokenEndpoint = "https://discord.com/api/oauth2/token";
88
91
  return {
89
92
  id: "discord",
90
93
  name: "Discord",
91
- createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
92
- const _scopes = options.disableDefaultScope ? [] : ["identify", "email"];
93
- if (scopes) _scopes.push(...scopes);
94
- if (options.scope) _scopes.push(...options.scope);
95
- const hasBotScope = _scopes.includes("bot");
94
+ callbackPath: "/callback/discord",
95
+ async createAuthorizationURL({
96
+ state,
97
+ scopes,
98
+ redirectURI,
99
+ additionalParams,
100
+ }) {
101
+ const requestedScopes = resolveRequestedScopes(
102
+ options,
103
+ DISCORD_DEFAULT_SCOPES,
104
+ scopes,
105
+ );
106
+ const hasBotScope = requestedScopes.includes("bot");
96
107
  return createAuthorizationURL({
97
108
  id: "discord",
98
109
  options,
99
110
  authorizationEndpoint: "https://discord.com/api/oauth2/authorize",
100
- scopes: _scopes,
111
+ scopes: requestedScopes,
101
112
  state,
102
113
  redirectURI,
103
114
  prompt: options.prompt || "none",
@@ -170,5 +181,5 @@ export const discord = (options: DiscordOptions) => {
170
181
  };
171
182
  },
172
183
  options,
173
- } satisfies OAuthProvider<DiscordProfile>;
184
+ } satisfies UpstreamProvider<DiscordProfile>;
174
185
  };
@@ -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,12 +26,15 @@ export interface DropboxOptions extends ProviderOptions<DropboxProfile> {
25
26
  accessType?: ("offline" | "online" | "legacy") | undefined;
26
27
  }
27
28
 
29
+ const DROPBOX_DEFAULT_SCOPES = ["account_info.read"];
30
+
28
31
  export const dropbox = (options: DropboxOptions) => {
29
32
  const tokenEndpoint = "https://api.dropboxapi.com/oauth2/token";
30
33
 
31
34
  return {
32
35
  id: "dropbox",
33
36
  name: "Dropbox",
37
+ callbackPath: "/callback/dropbox",
34
38
  createAuthorizationURL: async ({
35
39
  state,
36
40
  scopes,
@@ -38,14 +42,16 @@ export const dropbox = (options: DropboxOptions) => {
38
42
  redirectURI,
39
43
  additionalParams,
40
44
  }) => {
41
- const _scopes = options.disableDefaultScope ? [] : ["account_info.read"];
42
- if (options.scope) _scopes.push(...options.scope);
43
- if (scopes) _scopes.push(...scopes);
44
- return await createAuthorizationURL({
45
+ const requestedScopes = resolveRequestedScopes(
46
+ options,
47
+ DROPBOX_DEFAULT_SCOPES,
48
+ scopes,
49
+ );
50
+ return createAuthorizationURL({
45
51
  id: "dropbox",
46
52
  options,
47
53
  authorizationEndpoint: "https://www.dropbox.com/oauth2/authorize",
48
- scopes: _scopes,
54
+ scopes: requestedScopes,
49
55
  state,
50
56
  redirectURI,
51
57
  codeVerifier,
@@ -110,5 +116,5 @@ export const dropbox = (options: DropboxOptions) => {
110
116
  };
111
117
  },
112
118
  options,
113
- } satisfies OAuthProvider<DropboxProfile>;
119
+ } satisfies UpstreamProvider<DropboxProfile>;
114
120
  };
@@ -1,12 +1,13 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
- import { createRemoteJWKSet, decodeJwt, jwtVerify } from "jose";
2
+ import { createRemoteJWKSet, decodeJwt } from "jose";
3
3
  import { logger } from "../env";
4
4
  import { BetterAuthError } from "../error";
5
- import type { OAuthProvider, ProviderOptions } from "../oauth2";
5
+ import type { ProviderOptions, UpstreamProvider } from "../oauth2";
6
6
  import {
7
7
  createAuthorizationURL,
8
8
  getPrimaryClientId,
9
9
  refreshAccessToken,
10
+ resolveRequestedScopes,
10
11
  validateAuthorizationCode,
11
12
  } from "../oauth2";
12
13
  export interface FacebookProfile {
@@ -24,6 +25,58 @@ export interface FacebookProfile {
24
25
  };
25
26
  }
26
27
 
28
+ interface FacebookDebugTokenData {
29
+ app_id?: string;
30
+ is_valid?: boolean;
31
+ user_id?: string;
32
+ }
33
+
34
+ /**
35
+ * Validate an opaque Facebook access token against the configured app.
36
+ *
37
+ * Facebook access tokens are not audience-bound at the Graph `/me` endpoint: a
38
+ * token minted for any Facebook app returns that app's profile. Without this
39
+ * check, a token issued to an unrelated app could be presented to this
40
+ * app's direct sign-in path and accepted as proof of identity. We call the
41
+ * `debug_token` endpoint and require the token to be valid, bound to one of the
42
+ * configured client ids, and tied to a user.
43
+ *
44
+ * @see https://developers.facebook.com/docs/facebook-login/guides/access-tokens/debugging
45
+ *
46
+ * @returns the inspected token's `user_id` when the token is valid and bound to
47
+ * the configured app, otherwise `null`.
48
+ */
49
+ async function verifyFacebookAccessToken(
50
+ accessToken: string,
51
+ options: FacebookOptions,
52
+ ): Promise<string | null> {
53
+ const primaryClientId = getPrimaryClientId(options.clientId);
54
+ if (!primaryClientId || !options.clientSecret) {
55
+ return null;
56
+ }
57
+ const clientIds = Array.isArray(options.clientId)
58
+ ? options.clientId
59
+ : [options.clientId];
60
+ const appAccessToken = `${primaryClientId}|${options.clientSecret}`;
61
+ const { data, error } = await betterFetch<{ data?: FacebookDebugTokenData }>(
62
+ "https://graph.facebook.com/debug_token",
63
+ {
64
+ query: {
65
+ input_token: accessToken,
66
+ access_token: appAccessToken,
67
+ },
68
+ },
69
+ );
70
+ if (error || !data?.data) {
71
+ return null;
72
+ }
73
+ const { is_valid, app_id, user_id } = data.data;
74
+ if (is_valid !== true || !app_id || !clientIds.includes(app_id) || !user_id) {
75
+ return null;
76
+ }
77
+ return user_id;
78
+ }
79
+
27
80
  export interface FacebookOptions extends ProviderOptions<FacebookProfile> {
28
81
  clientId: string | string[];
29
82
  /**
@@ -39,10 +92,13 @@ export interface FacebookOptions extends ProviderOptions<FacebookProfile> {
39
92
  configId?: string | undefined;
40
93
  }
41
94
 
95
+ const FACEBOOK_DEFAULT_SCOPES = ["email", "public_profile"];
96
+
42
97
  export const facebook = (options: FacebookOptions) => {
43
98
  return {
44
99
  id: "facebook",
45
100
  name: "Facebook",
101
+ callbackPath: "/callback/facebook",
46
102
  async createAuthorizationURL({
47
103
  state,
48
104
  scopes,
@@ -56,16 +112,16 @@ export const facebook = (options: FacebookOptions) => {
56
112
  );
57
113
  throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
58
114
  }
59
- const _scopes = options.disableDefaultScope
60
- ? []
61
- : ["email", "public_profile"];
62
- if (options.scope) _scopes.push(...options.scope);
63
- if (scopes) _scopes.push(...scopes);
64
- return await createAuthorizationURL({
115
+ const requestedScopes = resolveRequestedScopes(
116
+ options,
117
+ FACEBOOK_DEFAULT_SCOPES,
118
+ scopes,
119
+ );
120
+ return createAuthorizationURL({
65
121
  id: "facebook",
66
122
  options,
67
123
  authorizationEndpoint: "https://www.facebook.com/v24.0/dialog/oauth",
68
- scopes: _scopes,
124
+ scopes: requestedScopes,
69
125
  state,
70
126
  redirectURI,
71
127
  loginHint,
@@ -83,46 +139,17 @@ export const facebook = (options: FacebookOptions) => {
83
139
  tokenEndpoint: "https://graph.facebook.com/v24.0/oauth/access_token",
84
140
  });
85
141
  },
86
- async verifyIdToken(token, nonce) {
87
- if (options.disableIdTokenSignIn) {
88
- return false;
89
- }
90
-
91
- if (options.verifyIdToken) {
92
- return options.verifyIdToken(token, nonce);
93
- }
94
-
95
- /* limited login */
96
- // check is limited token
97
- if (token.split(".").length === 3) {
98
- try {
99
- const { payload: jwtClaims } = await jwtVerify(
100
- token,
101
- createRemoteJWKSet(
102
- // https://developers.facebook.com/docs/facebook-login/limited-login/token/#jwks
103
- new URL(
104
- "https://limited.facebook.com/.well-known/oauth/openid/jwks/",
105
- ),
106
- ),
107
- {
108
- algorithms: ["RS256"],
109
- audience: options.clientId,
110
- issuer: "https://www.facebook.com",
111
- },
112
- );
113
-
114
- if (nonce && jwtClaims.nonce !== nonce) {
115
- return false;
116
- }
117
-
118
- return !!jwtClaims;
119
- } catch {
120
- return false;
121
- }
122
- }
123
-
124
- /* access_token */
125
- return true;
142
+ idToken: {
143
+ // https://developers.facebook.com/docs/facebook-login/limited-login/token/#jwks
144
+ jwks: createRemoteJWKSet(
145
+ new URL("https://limited.facebook.com/.well-known/oauth/openid/jwks/"),
146
+ ),
147
+ issuer: "https://www.facebook.com",
148
+ audience: options.clientId,
149
+ algorithms: ["RS256"],
150
+ // Facebook also accepts an opaque Graph access token on the client sign-in path;
151
+ // identity is then resolved by getUserInfo via the Graph API, which validates it.
152
+ allowOpaqueToken: true,
126
153
  },
127
154
  refreshAccessToken: options.refreshAccessToken
128
155
  ? options.refreshAccessToken
@@ -183,6 +210,21 @@ export const facebook = (options: FacebookOptions) => {
183
210
  };
184
211
  }
185
212
 
213
+ // The profile is fetched with `accessToken`, which is the credential
214
+ // that actually proves identity here. It is a separate request field
215
+ // from the `idToken` checked by the shared id_token verifier via the
216
+ // declarative `idToken` config. Since an opaque token is not app-bound
217
+ // at `/me`, validate this exact token against the configured app
218
+ // before trusting the profile it returns.
219
+ const accessToken = token.accessToken;
220
+ if (!accessToken) {
221
+ return null;
222
+ }
223
+ const tokenUserId = await verifyFacebookAccessToken(accessToken, options);
224
+ if (!tokenUserId) {
225
+ return null;
226
+ }
227
+
186
228
  const fields = [
187
229
  "id",
188
230
  "name",
@@ -195,13 +237,17 @@ export const facebook = (options: FacebookOptions) => {
195
237
  {
196
238
  auth: {
197
239
  type: "Bearer",
198
- token: token.accessToken,
240
+ token: accessToken,
199
241
  },
200
242
  },
201
243
  );
202
244
  if (error) {
203
245
  return null;
204
246
  }
247
+ // Bind the validated token to the profile it returned.
248
+ if (profile.id !== tokenUserId) {
249
+ return null;
250
+ }
205
251
  const userMap = await options.mapProfileToUser?.(profile);
206
252
  return {
207
253
  user: {
@@ -216,5 +262,5 @@ export const facebook = (options: FacebookOptions) => {
216
262
  };
217
263
  },
218
264
  options,
219
- } satisfies OAuthProvider<FacebookProfile>;
265
+ } satisfies UpstreamProvider<FacebookProfile>;
220
266
  };
@@ -1,10 +1,11 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
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
 
@@ -19,11 +20,14 @@ export interface FigmaOptions extends ProviderOptions<FigmaProfile> {
19
20
  clientId: string;
20
21
  }
21
22
 
23
+ const FIGMA_DEFAULT_SCOPES = ["current_user:read"];
24
+
22
25
  export const figma = (options: FigmaOptions) => {
23
26
  const tokenEndpoint = "https://api.figma.com/v1/oauth/token";
24
27
  return {
25
28
  id: "figma",
26
29
  name: "Figma",
30
+ callbackPath: "/callback/figma",
27
31
  async createAuthorizationURL({
28
32
  state,
29
33
  scopes,
@@ -41,22 +45,22 @@ export const figma = (options: FigmaOptions) => {
41
45
  throw new BetterAuthError("codeVerifier is required for Figma");
42
46
  }
43
47
 
44
- const _scopes = options.disableDefaultScope ? [] : ["current_user:read"];
45
- if (options.scope) _scopes.push(...options.scope);
46
- if (scopes) _scopes.push(...scopes);
48
+ const requestedScopes = resolveRequestedScopes(
49
+ options,
50
+ FIGMA_DEFAULT_SCOPES,
51
+ scopes,
52
+ );
47
53
 
48
- const url = await createAuthorizationURL({
54
+ return createAuthorizationURL({
49
55
  id: "figma",
50
56
  options,
51
57
  authorizationEndpoint: "https://www.figma.com/oauth",
52
- scopes: _scopes,
58
+ scopes: requestedScopes,
53
59
  state,
54
60
  codeVerifier,
55
61
  redirectURI,
56
62
  additionalParams,
57
63
  });
58
-
59
- return url;
60
64
  },
61
65
  validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
62
66
  return validateAuthorizationCode({
@@ -121,5 +125,5 @@ export const figma = (options: FigmaOptions) => {
121
125
  }
122
126
  },
123
127
  options,
124
- } satisfies OAuthProvider<FigmaProfile>;
128
+ } satisfies UpstreamProvider<FigmaProfile>;
125
129
  };
@@ -1,10 +1,11 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
2
  import { logger } from "../env";
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
  import { authorizationCodeRequest } from "../oauth2/validate-authorization-code";
10
11
 
@@ -58,11 +59,14 @@ export interface GithubProfile {
58
59
  export interface GithubOptions extends ProviderOptions<GithubProfile> {
59
60
  clientId: string;
60
61
  }
62
+ const GITHUB_DEFAULT_SCOPES = ["read:user", "user:email"];
63
+
61
64
  export const github = (options: GithubOptions) => {
62
65
  const tokenEndpoint = "https://github.com/login/oauth/access_token";
63
66
  return {
64
67
  id: "github",
65
68
  name: "GitHub",
69
+ callbackPath: "/callback/github",
66
70
  createAuthorizationURL({
67
71
  state,
68
72
  scopes,
@@ -71,16 +75,16 @@ export const github = (options: GithubOptions) => {
71
75
  redirectURI,
72
76
  additionalParams,
73
77
  }) {
74
- const _scopes = options.disableDefaultScope
75
- ? []
76
- : ["read:user", "user:email"];
77
- if (options.scope) _scopes.push(...options.scope);
78
- if (scopes) _scopes.push(...scopes);
78
+ const requestedScopes = resolveRequestedScopes(
79
+ options,
80
+ GITHUB_DEFAULT_SCOPES,
81
+ scopes,
82
+ );
79
83
  return createAuthorizationURL({
80
84
  id: "github",
81
85
  options,
82
86
  authorizationEndpoint: "https://github.com/login/oauth/authorize",
83
- scopes: _scopes,
87
+ scopes: requestedScopes,
84
88
  state,
85
89
  codeVerifier,
86
90
  redirectURI,
@@ -182,5 +186,5 @@ export const github = (options: GithubOptions) => {
182
186
  };
183
187
  },
184
188
  options,
185
- } satisfies OAuthProvider<GithubProfile>;
189
+ } satisfies UpstreamProvider<GithubProfile>;
186
190
  };
@@ -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
 
@@ -73,6 +74,8 @@ const issuerToEndpoints = (issuer?: string | undefined) => {
73
74
  };
74
75
  };
75
76
 
77
+ const GITLAB_DEFAULT_SCOPES = ["read_user"];
78
+
76
79
  export const gitlab = (options: GitlabOptions) => {
77
80
  const { authorizationEndpoint, tokenEndpoint, userinfoEndpoint } =
78
81
  issuerToEndpoints(options.issuer);
@@ -81,7 +84,8 @@ export const gitlab = (options: GitlabOptions) => {
81
84
  return {
82
85
  id: issuerId,
83
86
  name: issuerName,
84
- createAuthorizationURL: async ({
87
+ callbackPath: "/callback/gitlab",
88
+ createAuthorizationURL: ({
85
89
  state,
86
90
  scopes,
87
91
  codeVerifier,
@@ -89,14 +93,16 @@ export const gitlab = (options: GitlabOptions) => {
89
93
  redirectURI,
90
94
  additionalParams,
91
95
  }) => {
92
- const _scopes = options.disableDefaultScope ? [] : ["read_user"];
93
- if (options.scope) _scopes.push(...options.scope);
94
- if (scopes) _scopes.push(...scopes);
95
- return await createAuthorizationURL({
96
+ const requestedScopes = resolveRequestedScopes(
97
+ options,
98
+ GITLAB_DEFAULT_SCOPES,
99
+ scopes,
100
+ );
101
+ return createAuthorizationURL({
96
102
  id: issuerId,
97
103
  options,
98
104
  authorizationEndpoint,
99
- scopes: _scopes,
105
+ scopes: requestedScopes,
100
106
  state,
101
107
  redirectURI,
102
108
  codeVerifier,
@@ -153,5 +159,5 @@ export const gitlab = (options: GitlabOptions) => {
153
159
  };
154
160
  },
155
161
  options,
156
- } satisfies OAuthProvider<GitlabProfile>;
162
+ } satisfies UpstreamProvider<GitlabProfile>;
157
163
  };