@better-auth/core 1.7.0-beta.1 → 1.7.0-beta.10

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 (218) hide show
  1. package/dist/api/index.d.mts +44 -1
  2. package/dist/api/index.mjs +69 -4
  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 +91 -3
  7. package/dist/db/adapter/get-id-field.mjs +1 -1
  8. package/dist/db/adapter/index.d.mts +87 -3
  9. package/dist/db/adapter/types.d.mts +1 -1
  10. package/dist/db/get-tables.mjs +2 -1
  11. package/dist/db/type.d.mts +17 -0
  12. package/dist/env/env-impl.mjs +1 -1
  13. package/dist/error/codes.d.mts +1 -0
  14. package/dist/error/codes.mjs +1 -0
  15. package/dist/error/index.d.mts +7 -0
  16. package/dist/index.d.mts +2 -2
  17. package/dist/instrumentation/api.mjs +12 -0
  18. package/dist/instrumentation/noop.mjs +42 -0
  19. package/dist/instrumentation/pure.index.d.mts +7 -0
  20. package/dist/instrumentation/pure.index.mjs +7 -0
  21. package/dist/instrumentation/tracer.mjs +6 -3
  22. package/dist/oauth2/authorization-params.d.mts +12 -0
  23. package/dist/oauth2/authorization-params.mjs +12 -0
  24. package/dist/oauth2/basic-credentials.d.mts +30 -0
  25. package/dist/oauth2/basic-credentials.mjs +64 -0
  26. package/dist/oauth2/client-assertion.d.mts +38 -22
  27. package/dist/oauth2/client-assertion.mjs +63 -28
  28. package/dist/oauth2/client-credentials-token.d.mts +19 -40
  29. package/dist/oauth2/client-credentials-token.mjs +20 -31
  30. package/dist/oauth2/create-authorization-url.d.mts +11 -1
  31. package/dist/oauth2/create-authorization-url.mjs +27 -7
  32. package/dist/oauth2/dpop.d.mts +142 -0
  33. package/dist/oauth2/dpop.mjs +246 -0
  34. package/dist/oauth2/index.d.mts +14 -9
  35. package/dist/oauth2/index.mjs +12 -8
  36. package/dist/oauth2/oauth-provider.d.mts +144 -10
  37. package/dist/oauth2/refresh-access-token.d.mts +20 -40
  38. package/dist/oauth2/refresh-access-token.mjs +38 -36
  39. package/dist/oauth2/reject-redirects.mjs +65 -0
  40. package/dist/oauth2/token-endpoint-auth.d.mts +17 -0
  41. package/dist/oauth2/token-endpoint-auth.mjs +89 -0
  42. package/dist/oauth2/utils.d.mts +23 -1
  43. package/dist/oauth2/utils.mjs +48 -2
  44. package/dist/oauth2/validate-authorization-code.d.mts +17 -52
  45. package/dist/oauth2/validate-authorization-code.mjs +28 -34
  46. package/dist/oauth2/verify-id-token.d.mts +27 -0
  47. package/dist/oauth2/verify-id-token.mjs +62 -0
  48. package/dist/oauth2/verify.d.mts +88 -15
  49. package/dist/oauth2/verify.mjs +203 -25
  50. package/dist/social-providers/apple.d.mts +16 -4
  51. package/dist/social-providers/apple.mjs +17 -24
  52. package/dist/social-providers/atlassian.d.mts +5 -1
  53. package/dist/social-providers/atlassian.mjs +6 -3
  54. package/dist/social-providers/cognito.d.mts +27 -3
  55. package/dist/social-providers/cognito.mjs +15 -25
  56. package/dist/social-providers/discord.d.mts +7 -3
  57. package/dist/social-providers/discord.mjs +16 -3
  58. package/dist/social-providers/dropbox.d.mts +5 -1
  59. package/dist/social-providers/dropbox.mjs +5 -4
  60. package/dist/social-providers/facebook.d.mts +24 -5
  61. package/dist/social-providers/facebook.mjs +54 -20
  62. package/dist/social-providers/figma.d.mts +5 -1
  63. package/dist/social-providers/figma.mjs +4 -3
  64. package/dist/social-providers/github.d.mts +6 -2
  65. package/dist/social-providers/github.mjs +5 -4
  66. package/dist/social-providers/gitlab.d.mts +5 -1
  67. package/dist/social-providers/gitlab.mjs +3 -2
  68. package/dist/social-providers/google.d.mts +53 -5
  69. package/dist/social-providers/google.mjs +69 -25
  70. package/dist/social-providers/huggingface.d.mts +5 -1
  71. package/dist/social-providers/huggingface.mjs +3 -2
  72. package/dist/social-providers/index.d.mts +221 -45
  73. package/dist/social-providers/index.mjs +2 -2
  74. package/dist/social-providers/kakao.d.mts +5 -1
  75. package/dist/social-providers/kakao.mjs +3 -2
  76. package/dist/social-providers/kick.d.mts +5 -1
  77. package/dist/social-providers/kick.mjs +3 -2
  78. package/dist/social-providers/line.d.mts +8 -2
  79. package/dist/social-providers/line.mjs +5 -6
  80. package/dist/social-providers/linear.d.mts +5 -1
  81. package/dist/social-providers/linear.mjs +3 -2
  82. package/dist/social-providers/linkedin.d.mts +7 -3
  83. package/dist/social-providers/linkedin.mjs +4 -3
  84. package/dist/social-providers/microsoft-entra-id.d.mts +41 -7
  85. package/dist/social-providers/microsoft-entra-id.mjs +37 -28
  86. package/dist/social-providers/naver.d.mts +5 -1
  87. package/dist/social-providers/naver.mjs +3 -2
  88. package/dist/social-providers/notion.d.mts +5 -1
  89. package/dist/social-providers/notion.mjs +5 -2
  90. package/dist/social-providers/paybin.d.mts +5 -1
  91. package/dist/social-providers/paybin.mjs +4 -3
  92. package/dist/social-providers/paypal.d.mts +6 -2
  93. package/dist/social-providers/paypal.mjs +19 -14
  94. package/dist/social-providers/polar.d.mts +5 -1
  95. package/dist/social-providers/polar.mjs +3 -2
  96. package/dist/social-providers/railway.d.mts +5 -1
  97. package/dist/social-providers/railway.mjs +3 -2
  98. package/dist/social-providers/reddit.d.mts +5 -1
  99. package/dist/social-providers/reddit.mjs +7 -5
  100. package/dist/social-providers/roblox.d.mts +6 -2
  101. package/dist/social-providers/roblox.mjs +12 -2
  102. package/dist/social-providers/salesforce.d.mts +5 -1
  103. package/dist/social-providers/salesforce.mjs +4 -3
  104. package/dist/social-providers/slack.d.mts +6 -2
  105. package/dist/social-providers/slack.mjs +11 -8
  106. package/dist/social-providers/spotify.d.mts +5 -1
  107. package/dist/social-providers/spotify.mjs +3 -2
  108. package/dist/social-providers/tiktok.d.mts +5 -1
  109. package/dist/social-providers/tiktok.mjs +14 -2
  110. package/dist/social-providers/twitch.d.mts +5 -1
  111. package/dist/social-providers/twitch.mjs +3 -2
  112. package/dist/social-providers/twitter.d.mts +4 -1
  113. package/dist/social-providers/twitter.mjs +2 -1
  114. package/dist/social-providers/vercel.d.mts +5 -1
  115. package/dist/social-providers/vercel.mjs +3 -2
  116. package/dist/social-providers/vk.d.mts +5 -1
  117. package/dist/social-providers/vk.mjs +3 -2
  118. package/dist/social-providers/wechat.d.mts +5 -1
  119. package/dist/social-providers/wechat.mjs +8 -2
  120. package/dist/social-providers/zoom.d.mts +6 -2
  121. package/dist/social-providers/zoom.mjs +10 -17
  122. package/dist/types/context.d.mts +55 -6
  123. package/dist/types/index.d.mts +1 -1
  124. package/dist/types/init-options.d.mts +182 -12
  125. package/dist/types/plugin-client.d.mts +12 -2
  126. package/dist/utils/async.d.mts +22 -0
  127. package/dist/utils/async.mjs +32 -0
  128. package/dist/utils/host.d.mts +147 -0
  129. package/dist/utils/host.mjs +298 -0
  130. package/dist/utils/ip.d.mts +28 -5
  131. package/dist/utils/ip.mjs +118 -4
  132. package/dist/utils/is-api-error.d.mts +6 -0
  133. package/dist/utils/is-api-error.mjs +8 -0
  134. package/dist/utils/redirect-uri.d.mts +20 -0
  135. package/dist/utils/redirect-uri.mjs +48 -0
  136. package/dist/utils/string.d.mts +5 -1
  137. package/dist/utils/string.mjs +20 -1
  138. package/dist/utils/url.d.mts +18 -1
  139. package/dist/utils/url.mjs +34 -4
  140. package/package.json +26 -16
  141. package/src/api/index.ts +121 -5
  142. package/src/context/transaction.ts +45 -12
  143. package/src/db/adapter/factory.ts +193 -7
  144. package/src/db/adapter/get-id-field.ts +2 -2
  145. package/src/db/adapter/index.ts +85 -2
  146. package/src/db/adapter/types.ts +2 -0
  147. package/src/db/get-tables.ts +9 -1
  148. package/src/db/schema/account.ts +4 -1
  149. package/src/db/schema/user.ts +3 -0
  150. package/src/db/type.ts +17 -0
  151. package/src/env/env-impl.ts +1 -2
  152. package/src/error/codes.ts +1 -0
  153. package/src/error/index.ts +9 -0
  154. package/src/instrumentation/api.ts +17 -0
  155. package/src/instrumentation/noop.ts +74 -0
  156. package/src/instrumentation/pure.index.ts +31 -0
  157. package/src/instrumentation/tracer.ts +8 -3
  158. package/src/oauth2/authorization-params.ts +28 -0
  159. package/src/oauth2/basic-credentials.ts +87 -0
  160. package/src/oauth2/client-assertion.ts +131 -58
  161. package/src/oauth2/client-credentials-token.ts +50 -74
  162. package/src/oauth2/create-authorization-url.ts +33 -7
  163. package/src/oauth2/dpop.ts +568 -0
  164. package/src/oauth2/index.ts +81 -11
  165. package/src/oauth2/oauth-provider.ts +153 -11
  166. package/src/oauth2/refresh-access-token.ts +82 -83
  167. package/src/oauth2/reject-redirects.ts +75 -0
  168. package/src/oauth2/token-endpoint-auth.ts +221 -0
  169. package/src/oauth2/utils.ts +72 -5
  170. package/src/oauth2/validate-authorization-code.ts +69 -89
  171. package/src/oauth2/verify-id-token.ts +115 -0
  172. package/src/oauth2/verify.ts +409 -78
  173. package/src/social-providers/apple.ts +30 -40
  174. package/src/social-providers/atlassian.ts +8 -1
  175. package/src/social-providers/cognito.ts +36 -39
  176. package/src/social-providers/discord.ts +22 -18
  177. package/src/social-providers/dropbox.ts +7 -5
  178. package/src/social-providers/facebook.ts +108 -52
  179. package/src/social-providers/figma.ts +8 -1
  180. package/src/social-providers/github.ts +5 -3
  181. package/src/social-providers/gitlab.ts +2 -0
  182. package/src/social-providers/google.ts +141 -39
  183. package/src/social-providers/huggingface.ts +8 -1
  184. package/src/social-providers/kakao.ts +2 -1
  185. package/src/social-providers/kick.ts +8 -1
  186. package/src/social-providers/line.ts +25 -27
  187. package/src/social-providers/linear.ts +8 -1
  188. package/src/social-providers/linkedin.ts +5 -3
  189. package/src/social-providers/microsoft-entra-id.ts +100 -55
  190. package/src/social-providers/naver.ts +2 -1
  191. package/src/social-providers/notion.ts +8 -1
  192. package/src/social-providers/paybin.ts +2 -0
  193. package/src/social-providers/paypal.ts +28 -17
  194. package/src/social-providers/polar.ts +8 -1
  195. package/src/social-providers/railway.ts +8 -1
  196. package/src/social-providers/reddit.ts +9 -4
  197. package/src/social-providers/roblox.ts +16 -11
  198. package/src/social-providers/salesforce.ts +8 -1
  199. package/src/social-providers/slack.ts +15 -9
  200. package/src/social-providers/spotify.ts +8 -1
  201. package/src/social-providers/tiktok.ts +22 -9
  202. package/src/social-providers/twitch.ts +2 -1
  203. package/src/social-providers/twitter.ts +1 -0
  204. package/src/social-providers/vercel.ts +8 -1
  205. package/src/social-providers/vk.ts +8 -1
  206. package/src/social-providers/wechat.ts +17 -2
  207. package/src/social-providers/zoom.ts +15 -19
  208. package/src/types/context.ts +57 -5
  209. package/src/types/index.ts +7 -0
  210. package/src/types/init-options.ts +204 -14
  211. package/src/types/plugin-client.ts +16 -2
  212. package/src/utils/async.ts +53 -0
  213. package/src/utils/host.ts +425 -0
  214. package/src/utils/ip.ts +197 -13
  215. package/src/utils/is-api-error.ts +10 -0
  216. package/src/utils/redirect-uri.ts +54 -0
  217. package/src/utils/string.ts +37 -0
  218. package/src/utils/url.ts +38 -4
@@ -35,7 +35,13 @@ export const atlassian = (options: AtlassianOptions) => {
35
35
  id: "atlassian",
36
36
  name: "Atlassian",
37
37
 
38
- async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
38
+ async createAuthorizationURL({
39
+ state,
40
+ scopes,
41
+ codeVerifier,
42
+ redirectURI,
43
+ additionalParams,
44
+ }) {
39
45
  if (!options.clientId || !options.clientSecret) {
40
46
  logger.error("Client Id and Secret are required for Atlassian");
41
47
  throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
@@ -59,6 +65,7 @@ export const atlassian = (options: AtlassianOptions) => {
59
65
  codeVerifier,
60
66
  redirectURI,
61
67
  additionalParams: {
68
+ ...(additionalParams ?? {}),
62
69
  audience: "api.atlassian.com",
63
70
  },
64
71
  prompt: options.prompt,
@@ -1,10 +1,11 @@
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
5
  import type { OAuthProvider, ProviderOptions } from "../oauth2";
6
6
  import {
7
7
  createAuthorizationURL,
8
+ getPrimaryClientId,
8
9
  refreshAccessToken,
9
10
  validateAuthorizationCode,
10
11
  } from "../oauth2";
@@ -30,7 +31,7 @@ export interface CognitoProfile {
30
31
  }
31
32
 
32
33
  export interface CognitoOptions extends ProviderOptions<CognitoProfile> {
33
- clientId: string;
34
+ clientId: string | string[];
34
35
  /**
35
36
  * The Cognito domain (e.g., "your-app.auth.us-east-1.amazoncognito.com")
36
37
  */
@@ -41,6 +42,19 @@ export interface CognitoOptions extends ProviderOptions<CognitoProfile> {
41
42
  region: string;
42
43
  userPoolId: string;
43
44
  requireClientSecret?: boolean | undefined;
45
+ /**
46
+ * Skip the Cognito hosted-UI identity-provider picker by preselecting an
47
+ * IdP (maps to the `identity_provider` query parameter on the authorize
48
+ * request). Accepts `"COGNITO"`, a SAML/OIDC provider name configured on
49
+ * the User Pool, or one of the social providers (`"Google"`, `"Facebook"`,
50
+ * `"LoginWithAmazon"`, `"SignInWithApple"`).
51
+ *
52
+ * Per-request overrides via `signIn.social({ additionalParams: { identity_provider } })`
53
+ * take precedence over this value.
54
+ *
55
+ * @see https://docs.aws.amazon.com/cognito/latest/developerguide/authorization-endpoint.html
56
+ */
57
+ identityProvider?: string | undefined;
44
58
  }
45
59
 
46
60
  export const cognito = (options: CognitoOptions) => {
@@ -59,8 +73,14 @@ export const cognito = (options: CognitoOptions) => {
59
73
  return {
60
74
  id: "cognito",
61
75
  name: "Cognito",
62
- async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
63
- if (!options.clientId) {
76
+ async createAuthorizationURL({
77
+ state,
78
+ scopes,
79
+ codeVerifier,
80
+ redirectURI,
81
+ additionalParams,
82
+ }) {
83
+ if (!getPrimaryClientId(options.clientId)) {
64
84
  logger.error(
65
85
  "ClientId is required for Amazon Cognito. Make sure to provide them in the options.",
66
86
  );
@@ -90,6 +110,12 @@ export const cognito = (options: CognitoOptions) => {
90
110
  codeVerifier,
91
111
  redirectURI,
92
112
  prompt: options.prompt,
113
+ additionalParams: {
114
+ ...(options.identityProvider
115
+ ? { identity_provider: options.identityProvider }
116
+ : {}),
117
+ ...(additionalParams ?? {}),
118
+ },
93
119
  });
94
120
  // AWS Cognito requires scopes to be encoded with %20 instead of +
95
121
  // URLSearchParams encodes spaces as + by default, so we need to fix this
@@ -129,41 +155,12 @@ export const cognito = (options: CognitoOptions) => {
129
155
  });
130
156
  },
131
157
 
132
- async verifyIdToken(token, nonce) {
133
- if (options.disableIdTokenSignIn) {
134
- return false;
135
- }
136
- if (options.verifyIdToken) {
137
- return options.verifyIdToken(token, nonce);
138
- }
139
-
140
- try {
141
- const decodedHeader = decodeProtectedHeader(token);
142
- const { kid, alg: jwtAlg } = decodedHeader;
143
- if (!kid || !jwtAlg) return false;
144
-
145
- const publicKey = await getCognitoPublicKey(
146
- kid,
147
- options.region,
148
- options.userPoolId,
149
- );
150
- const expectedIssuer = `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`;
151
-
152
- const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
153
- algorithms: [jwtAlg],
154
- issuer: expectedIssuer,
155
- audience: options.clientId,
156
- maxTokenAge: "1h",
157
- });
158
-
159
- if (nonce && jwtClaims.nonce !== nonce) {
160
- return false;
161
- }
162
- return true;
163
- } catch (error) {
164
- logger.error("Failed to verify ID token:", error);
165
- return false;
166
- }
158
+ idToken: {
159
+ jwks: (header) =>
160
+ getCognitoPublicKey(header.kid!, options.region, options.userPoolId),
161
+ issuer: `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`,
162
+ audience: options.clientId,
163
+ maxTokenAge: "1h",
167
164
  },
168
165
 
169
166
  async getUserInfo(token) {
@@ -1,6 +1,10 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
2
  import type { OAuthProvider, ProviderOptions } from "../oauth2";
3
- import { refreshAccessToken, validateAuthorizationCode } from "../oauth2";
3
+ import {
4
+ createAuthorizationURL,
5
+ refreshAccessToken,
6
+ validateAuthorizationCode,
7
+ } from "../oauth2";
4
8
  export interface DiscordProfile extends Record<string, any> {
5
9
  /** the user's id (i.e. the numerical snowflake) */
6
10
  id: string;
@@ -41,7 +45,7 @@ export interface DiscordProfile extends Record<string, any> {
41
45
  /** whether the email on this account has been verified */
42
46
  verified: boolean;
43
47
  /** the user's email */
44
- email: string;
48
+ email?: string | null;
45
49
  /**
46
50
  * the flags on a user's account:
47
51
  * https://discord.com/developers/docs/resources/user#user-object-user-flags
@@ -84,26 +88,26 @@ export const discord = (options: DiscordOptions) => {
84
88
  return {
85
89
  id: "discord",
86
90
  name: "Discord",
87
- createAuthorizationURL({ state, scopes, redirectURI }) {
91
+ createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
88
92
  const _scopes = options.disableDefaultScope ? [] : ["identify", "email"];
89
93
  if (scopes) _scopes.push(...scopes);
90
94
  if (options.scope) _scopes.push(...options.scope);
91
95
  const hasBotScope = _scopes.includes("bot");
92
- const permissionsParam =
93
- hasBotScope && options.permissions !== undefined
94
- ? `&permissions=${options.permissions}`
95
- : "";
96
- return new URL(
97
- `https://discord.com/api/oauth2/authorize?scope=${_scopes.join(
98
- "+",
99
- )}&response_type=code&client_id=${
100
- options.clientId
101
- }&redirect_uri=${encodeURIComponent(
102
- options.redirectURI || redirectURI,
103
- )}&state=${state}&prompt=${
104
- options.prompt || "none"
105
- }${permissionsParam}`,
106
- );
96
+ return createAuthorizationURL({
97
+ id: "discord",
98
+ options,
99
+ authorizationEndpoint: "https://discord.com/api/oauth2/authorize",
100
+ scopes: _scopes,
101
+ state,
102
+ redirectURI,
103
+ prompt: options.prompt || "none",
104
+ additionalParams: {
105
+ ...(hasBotScope && options.permissions !== undefined
106
+ ? { permissions: String(options.permissions) }
107
+ : {}),
108
+ ...(additionalParams ?? {}),
109
+ },
110
+ });
107
111
  },
108
112
  validateAuthorizationCode: async ({ code, redirectURI }) => {
109
113
  return validateAuthorizationCode({
@@ -36,14 +36,11 @@ export const dropbox = (options: DropboxOptions) => {
36
36
  scopes,
37
37
  codeVerifier,
38
38
  redirectURI,
39
+ additionalParams,
39
40
  }) => {
40
41
  const _scopes = options.disableDefaultScope ? [] : ["account_info.read"];
41
42
  if (options.scope) _scopes.push(...options.scope);
42
43
  if (scopes) _scopes.push(...scopes);
43
- const additionalParams: Record<string, string> = {};
44
- if (options.accessType) {
45
- additionalParams.token_access_type = options.accessType;
46
- }
47
44
  return await createAuthorizationURL({
48
45
  id: "dropbox",
49
46
  options,
@@ -52,7 +49,12 @@ export const dropbox = (options: DropboxOptions) => {
52
49
  state,
53
50
  redirectURI,
54
51
  codeVerifier,
55
- additionalParams,
52
+ additionalParams: {
53
+ ...(options.accessType
54
+ ? { token_access_type: options.accessType }
55
+ : {}),
56
+ ...(additionalParams ?? {}),
57
+ },
56
58
  });
57
59
  },
58
60
  validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
@@ -1,16 +1,19 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
- import { createRemoteJWKSet, decodeJwt, jwtVerify } from "jose";
2
+ import { createRemoteJWKSet, decodeJwt } from "jose";
3
+ import { logger } from "../env";
4
+ import { BetterAuthError } from "../error";
3
5
  import type { OAuthProvider, ProviderOptions } from "../oauth2";
4
6
  import {
5
7
  createAuthorizationURL,
8
+ getPrimaryClientId,
6
9
  refreshAccessToken,
7
10
  validateAuthorizationCode,
8
11
  } from "../oauth2";
9
12
  export interface FacebookProfile {
10
13
  id: string;
11
14
  name: string;
12
- email: string;
13
- email_verified: boolean;
15
+ email?: string;
16
+ email_verified?: boolean;
14
17
  picture: {
15
18
  data: {
16
19
  height: number;
@@ -21,8 +24,60 @@ export interface FacebookProfile {
21
24
  };
22
25
  }
23
26
 
27
+ interface FacebookDebugTokenData {
28
+ app_id?: string;
29
+ is_valid?: boolean;
30
+ user_id?: string;
31
+ }
32
+
33
+ /**
34
+ * Validate an opaque Facebook access token against the configured app.
35
+ *
36
+ * Facebook access tokens are not audience-bound at the Graph `/me` endpoint: a
37
+ * token minted for any Facebook app returns that app's profile. Without this
38
+ * check, a token issued to an unrelated app could be presented to this
39
+ * app's direct sign-in path and accepted as proof of identity. We call the
40
+ * `debug_token` endpoint and require the token to be valid, bound to one of the
41
+ * configured client ids, and tied to a user.
42
+ *
43
+ * @see https://developers.facebook.com/docs/facebook-login/guides/access-tokens/debugging
44
+ *
45
+ * @returns the inspected token's `user_id` when the token is valid and bound to
46
+ * the configured app, otherwise `null`.
47
+ */
48
+ async function verifyFacebookAccessToken(
49
+ accessToken: string,
50
+ options: FacebookOptions,
51
+ ): Promise<string | null> {
52
+ const primaryClientId = getPrimaryClientId(options.clientId);
53
+ if (!primaryClientId || !options.clientSecret) {
54
+ return null;
55
+ }
56
+ const clientIds = Array.isArray(options.clientId)
57
+ ? options.clientId
58
+ : [options.clientId];
59
+ const appAccessToken = `${primaryClientId}|${options.clientSecret}`;
60
+ const { data, error } = await betterFetch<{ data?: FacebookDebugTokenData }>(
61
+ "https://graph.facebook.com/debug_token",
62
+ {
63
+ query: {
64
+ input_token: accessToken,
65
+ access_token: appAccessToken,
66
+ },
67
+ },
68
+ );
69
+ if (error || !data?.data) {
70
+ return null;
71
+ }
72
+ const { is_valid, app_id, user_id } = data.data;
73
+ if (is_valid !== true || !app_id || !clientIds.includes(app_id) || !user_id) {
74
+ return null;
75
+ }
76
+ return user_id;
77
+ }
78
+
24
79
  export interface FacebookOptions extends ProviderOptions<FacebookProfile> {
25
- clientId: string;
80
+ clientId: string | string[];
26
81
  /**
27
82
  * Extend list of fields to retrieve from the Facebook user profile.
28
83
  *
@@ -40,7 +95,19 @@ export const facebook = (options: FacebookOptions) => {
40
95
  return {
41
96
  id: "facebook",
42
97
  name: "Facebook",
43
- async createAuthorizationURL({ state, scopes, redirectURI, loginHint }) {
98
+ async createAuthorizationURL({
99
+ state,
100
+ scopes,
101
+ redirectURI,
102
+ loginHint,
103
+ additionalParams,
104
+ }) {
105
+ if (!getPrimaryClientId(options.clientId) || !options.clientSecret) {
106
+ logger.error(
107
+ "Client ID and client secret are required for Facebook. Make sure to provide them in the options.",
108
+ );
109
+ throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
110
+ }
44
111
  const _scopes = options.disableDefaultScope
45
112
  ? []
46
113
  : ["email", "public_profile"];
@@ -54,11 +121,10 @@ export const facebook = (options: FacebookOptions) => {
54
121
  state,
55
122
  redirectURI,
56
123
  loginHint,
57
- additionalParams: options.configId
58
- ? {
59
- config_id: options.configId,
60
- }
61
- : {},
124
+ additionalParams: {
125
+ ...(options.configId ? { config_id: options.configId } : {}),
126
+ ...(additionalParams ?? {}),
127
+ },
62
128
  });
63
129
  },
64
130
  validateAuthorizationCode: async ({ code, redirectURI }) => {
@@ -69,46 +135,17 @@ export const facebook = (options: FacebookOptions) => {
69
135
  tokenEndpoint: "https://graph.facebook.com/v24.0/oauth/access_token",
70
136
  });
71
137
  },
72
- async verifyIdToken(token, nonce) {
73
- if (options.disableIdTokenSignIn) {
74
- return false;
75
- }
76
-
77
- if (options.verifyIdToken) {
78
- return options.verifyIdToken(token, nonce);
79
- }
80
-
81
- /* limited login */
82
- // check is limited token
83
- if (token.split(".").length === 3) {
84
- try {
85
- const { payload: jwtClaims } = await jwtVerify(
86
- token,
87
- createRemoteJWKSet(
88
- // https://developers.facebook.com/docs/facebook-login/limited-login/token/#jwks
89
- new URL(
90
- "https://limited.facebook.com/.well-known/oauth/openid/jwks/",
91
- ),
92
- ),
93
- {
94
- algorithms: ["RS256"],
95
- audience: options.clientId,
96
- issuer: "https://www.facebook.com",
97
- },
98
- );
99
-
100
- if (nonce && jwtClaims.nonce !== nonce) {
101
- return false;
102
- }
103
-
104
- return !!jwtClaims;
105
- } catch {
106
- return false;
107
- }
108
- }
109
-
110
- /* access_token */
111
- return true;
138
+ idToken: {
139
+ // https://developers.facebook.com/docs/facebook-login/limited-login/token/#jwks
140
+ jwks: createRemoteJWKSet(
141
+ new URL("https://limited.facebook.com/.well-known/oauth/openid/jwks/"),
142
+ ),
143
+ issuer: "https://www.facebook.com",
144
+ audience: options.clientId,
145
+ algorithms: ["RS256"],
146
+ // Facebook also accepts an opaque Graph access token on the client sign-in path;
147
+ // identity is then resolved by getUserInfo via the Graph API, which validates it.
148
+ allowOpaqueToken: true,
112
149
  },
113
150
  refreshAccessToken: options.refreshAccessToken
114
151
  ? options.refreshAccessToken
@@ -169,6 +206,21 @@ export const facebook = (options: FacebookOptions) => {
169
206
  };
170
207
  }
171
208
 
209
+ // The profile is fetched with `accessToken`, which is the credential
210
+ // that actually proves identity here. It is a separate request field
211
+ // from the `idToken` checked by the shared id_token verifier via the
212
+ // declarative `idToken` config. Since an opaque token is not app-bound
213
+ // at `/me`, validate this exact token against the configured app
214
+ // before trusting the profile it returns.
215
+ const accessToken = token.accessToken;
216
+ if (!accessToken) {
217
+ return null;
218
+ }
219
+ const tokenUserId = await verifyFacebookAccessToken(accessToken, options);
220
+ if (!tokenUserId) {
221
+ return null;
222
+ }
223
+
172
224
  const fields = [
173
225
  "id",
174
226
  "name",
@@ -181,13 +233,17 @@ export const facebook = (options: FacebookOptions) => {
181
233
  {
182
234
  auth: {
183
235
  type: "Bearer",
184
- token: token.accessToken,
236
+ token: accessToken,
185
237
  },
186
238
  },
187
239
  );
188
240
  if (error) {
189
241
  return null;
190
242
  }
243
+ // Bind the validated token to the profile it returned.
244
+ if (profile.id !== tokenUserId) {
245
+ return null;
246
+ }
191
247
  const userMap = await options.mapProfileToUser?.(profile);
192
248
  return {
193
249
  user: {
@@ -195,7 +251,7 @@ export const facebook = (options: FacebookOptions) => {
195
251
  name: profile.name,
196
252
  email: profile.email,
197
253
  image: profile.picture.data.url,
198
- emailVerified: profile.email_verified,
254
+ emailVerified: profile.email_verified ?? false,
199
255
  ...userMap,
200
256
  },
201
257
  data: profile,
@@ -24,7 +24,13 @@ export const figma = (options: FigmaOptions) => {
24
24
  return {
25
25
  id: "figma",
26
26
  name: "Figma",
27
- async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
27
+ async createAuthorizationURL({
28
+ state,
29
+ scopes,
30
+ codeVerifier,
31
+ redirectURI,
32
+ additionalParams,
33
+ }) {
28
34
  if (!options.clientId || !options.clientSecret) {
29
35
  logger.error(
30
36
  "Client Id and Client Secret are required for Figma. Make sure to provide them in the options.",
@@ -47,6 +53,7 @@ export const figma = (options: FigmaOptions) => {
47
53
  state,
48
54
  codeVerifier,
49
55
  redirectURI,
56
+ additionalParams,
50
57
  });
51
58
 
52
59
  return url;
@@ -6,7 +6,7 @@ import {
6
6
  getOAuth2Tokens,
7
7
  refreshAccessToken,
8
8
  } from "../oauth2";
9
- import { createAuthorizationCodeRequest } from "../oauth2/validate-authorization-code";
9
+ import { authorizationCodeRequest } from "../oauth2/validate-authorization-code";
10
10
 
11
11
  export interface GithubProfile {
12
12
  login: string;
@@ -31,7 +31,7 @@ export interface GithubProfile {
31
31
  company: string;
32
32
  blog: string;
33
33
  location: string;
34
- email: string;
34
+ email: string | null;
35
35
  hireable: boolean;
36
36
  bio: string;
37
37
  twitter_username: string;
@@ -69,6 +69,7 @@ export const github = (options: GithubOptions) => {
69
69
  loginHint,
70
70
  codeVerifier,
71
71
  redirectURI,
72
+ additionalParams,
72
73
  }) {
73
74
  const _scopes = options.disableDefaultScope
74
75
  ? []
@@ -85,10 +86,11 @@ export const github = (options: GithubOptions) => {
85
86
  redirectURI,
86
87
  loginHint,
87
88
  prompt: options.prompt,
89
+ additionalParams,
88
90
  });
89
91
  },
90
92
  validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
91
- const { body, headers: requestHeaders } = createAuthorizationCodeRequest({
93
+ const { body, headers: requestHeaders } = await authorizationCodeRequest({
92
94
  code,
93
95
  codeVerifier,
94
96
  redirectURI,
@@ -87,6 +87,7 @@ export const gitlab = (options: GitlabOptions) => {
87
87
  codeVerifier,
88
88
  loginHint,
89
89
  redirectURI,
90
+ additionalParams,
90
91
  }) => {
91
92
  const _scopes = options.disableDefaultScope ? [] : ["read_user"];
92
93
  if (options.scope) _scopes.push(...options.scope);
@@ -100,6 +101,7 @@ export const gitlab = (options: GitlabOptions) => {
100
101
  redirectURI,
101
102
  codeVerifier,
102
103
  loginHint,
104
+ additionalParams,
103
105
  });
104
106
  },
105
107
  validateAuthorizationCode: async ({ code, redirectURI, codeVerifier }) => {