@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
@@ -1,61 +1,41 @@
1
- import { ClientAssertionConfig } from "./client-assertion.mjs";
2
1
  import { AwaitableFunction } from "../types/helper.mjs";
3
2
  import { OAuth2Tokens, ProviderOptions } from "./oauth-provider.mjs";
3
+ import { TokenEndpointAuth, TokenEndpointSecretAuthentication } from "./token-endpoint-auth.mjs";
4
4
 
5
5
  //#region src/oauth2/refresh-access-token.d.ts
6
- declare function refreshAccessTokenRequest({
7
- refreshToken,
8
- options,
9
- authentication,
10
- clientAssertion,
11
- tokenEndpoint,
12
- extraParams,
13
- resource
14
- }: {
6
+ interface RefreshAccessTokenRequestInput {
15
7
  refreshToken: string;
16
8
  options: AwaitableFunction<Partial<ProviderOptions>>;
17
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
18
- clientAssertion?: ClientAssertionConfig | undefined; /** Token endpoint URL. Used as the JWT `aud` claim when signing assertions. */
9
+ authentication?: TokenEndpointSecretAuthentication | undefined;
10
+ tokenEndpointAuth?: TokenEndpointAuth | undefined;
19
11
  tokenEndpoint?: string | undefined;
20
12
  extraParams?: Record<string, string> | undefined;
21
13
  resource?: (string | string[]) | undefined;
22
- }): Promise<{
23
- body: URLSearchParams;
24
- headers: Record<string, any>;
25
- }>;
26
- /**
27
- * @deprecated use async'd refreshAccessTokenRequest instead
28
- */
29
- declare function createRefreshAccessTokenRequest({
14
+ }
15
+ interface RefreshAccessTokenInput extends RefreshAccessTokenRequestInput {
16
+ options: Partial<ProviderOptions>;
17
+ tokenEndpoint: string;
18
+ }
19
+ declare function refreshAccessTokenRequest({
30
20
  refreshToken,
31
21
  options,
32
22
  authentication,
23
+ tokenEndpointAuth,
24
+ tokenEndpoint,
33
25
  extraParams,
34
26
  resource
35
- }: {
36
- refreshToken: string;
37
- options: ProviderOptions;
38
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
39
- extraParams?: Record<string, string> | undefined;
40
- resource?: (string | string[]) | undefined;
41
- }): {
27
+ }: RefreshAccessTokenRequestInput): Promise<{
42
28
  body: URLSearchParams;
43
- headers: Record<string, any>;
44
- };
29
+ headers: Record<string, string>;
30
+ }>;
45
31
  declare function refreshAccessToken({
46
32
  refreshToken,
47
33
  options,
48
34
  tokenEndpoint,
49
35
  authentication,
50
- clientAssertion,
51
- extraParams
52
- }: {
53
- refreshToken: string;
54
- options: Partial<ProviderOptions>;
55
- tokenEndpoint: string;
56
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
57
- clientAssertion?: ClientAssertionConfig | undefined;
58
- extraParams?: Record<string, string> | undefined;
59
- }): Promise<OAuth2Tokens>;
36
+ tokenEndpointAuth,
37
+ extraParams,
38
+ resource
39
+ }: RefreshAccessTokenInput): Promise<OAuth2Tokens>;
60
40
  //#endregion
61
- export { createRefreshAccessTokenRequest, refreshAccessToken, refreshAccessTokenRequest };
41
+ export { refreshAccessToken, refreshAccessTokenRequest };
@@ -1,33 +1,41 @@
1
- import { resolveAssertionParams } from "./client-assertion.mjs";
2
- import { base64 } from "@better-auth/utils/base64";
3
- import { betterFetch } from "@better-fetch/fetch";
1
+ import { parseScopeField } from "./utils.mjs";
2
+ import { fetchRefusingRedirects } from "./reject-redirects.mjs";
3
+ import { applyTokenEndpointAuth } from "./token-endpoint-auth.mjs";
4
4
  //#region src/oauth2/refresh-access-token.ts
5
- async function refreshAccessTokenRequest({ refreshToken, options, authentication, clientAssertion, tokenEndpoint, extraParams, resource }) {
5
+ const BLOCKED_REFRESH_TOKEN_PARAMS_SET = new Set([
6
+ "grant_type",
7
+ "refresh_token",
8
+ "__proto__",
9
+ "constructor",
10
+ "prototype"
11
+ ]);
12
+ async function refreshAccessTokenRequest({ refreshToken, options, authentication, tokenEndpointAuth, tokenEndpoint, extraParams, resource }) {
6
13
  options = typeof options === "function" ? await options() : options;
7
- if (authentication === "private_key_jwt") {
8
- if (!clientAssertion) throw new Error("private_key_jwt authentication requires a clientAssertion configuration");
9
- const assertionParams = await resolveAssertionParams({
10
- clientAssertion,
11
- clientId: Array.isArray(options.clientId) ? options.clientId[0] : options.clientId,
12
- tokenEndpoint
13
- });
14
- extraParams = {
15
- ...extraParams,
16
- ...assertionParams
17
- };
18
- }
19
- return createRefreshAccessTokenRequest({
14
+ const request = buildRefreshAccessTokenRequest({
20
15
  refreshToken,
21
16
  options,
22
- authentication,
23
17
  extraParams,
24
18
  resource
25
19
  });
20
+ await applyTokenEndpointAuth({
21
+ body: request.body,
22
+ headers: request.headers,
23
+ options,
24
+ tokenEndpoint: tokenEndpoint ?? "",
25
+ grantType: "refresh_token",
26
+ tokenEndpointAuth,
27
+ authentication
28
+ });
29
+ return request;
26
30
  }
27
- /**
28
- * @deprecated use async'd refreshAccessTokenRequest instead
29
- */
30
- function createRefreshAccessTokenRequest({ refreshToken, options, authentication, extraParams, resource }) {
31
+ function applyRefreshExtraParams(body, extraParams) {
32
+ if (!extraParams) return;
33
+ for (const [key, value] of Object.entries(extraParams)) {
34
+ if (BLOCKED_REFRESH_TOKEN_PARAMS_SET.has(key)) continue;
35
+ body.set(key, value);
36
+ }
37
+ }
38
+ function buildRefreshAccessTokenRequest({ refreshToken, options, extraParams, resource }) {
31
39
  const body = new URLSearchParams();
32
40
  const headers = {
33
41
  "content-type": "application/x-www-form-urlencoded",
@@ -35,31 +43,25 @@ function createRefreshAccessTokenRequest({ refreshToken, options, authentication
35
43
  };
36
44
  body.set("grant_type", "refresh_token");
37
45
  body.set("refresh_token", refreshToken);
38
- const primaryClientId = Array.isArray(options.clientId) ? options.clientId[0] : options.clientId;
39
- if (authentication === "basic") if (primaryClientId) headers["authorization"] = "Basic " + base64.encode(`${primaryClientId}:${options.clientSecret ?? ""}`);
40
- else headers["authorization"] = "Basic " + base64.encode(`:${options.clientSecret ?? ""}`);
41
- else {
42
- body.set("client_id", primaryClientId);
43
- if (authentication !== "private_key_jwt" && options.clientSecret) body.set("client_secret", options.clientSecret);
44
- }
45
46
  if (resource) if (typeof resource === "string") body.append("resource", resource);
46
47
  else for (const _resource of resource) body.append("resource", _resource);
47
- if (extraParams) for (const [key, value] of Object.entries(extraParams)) body.set(key, value);
48
+ if (extraParams) applyRefreshExtraParams(body, extraParams);
48
49
  return {
49
50
  body,
50
51
  headers
51
52
  };
52
53
  }
53
- async function refreshAccessToken({ refreshToken, options, tokenEndpoint, authentication, clientAssertion, extraParams }) {
54
+ async function refreshAccessToken({ refreshToken, options, tokenEndpoint, authentication, tokenEndpointAuth, extraParams, resource }) {
54
55
  const { body, headers } = await refreshAccessTokenRequest({
55
56
  refreshToken,
56
57
  options,
57
58
  authentication,
58
- clientAssertion,
59
+ tokenEndpointAuth,
59
60
  tokenEndpoint,
60
- extraParams
61
+ extraParams,
62
+ resource
61
63
  });
62
- const { data, error } = await betterFetch(tokenEndpoint, {
64
+ const { data, error } = await fetchRefusingRedirects(tokenEndpoint, {
63
65
  method: "POST",
64
66
  body,
65
67
  headers
@@ -69,7 +71,7 @@ async function refreshAccessToken({ refreshToken, options, tokenEndpoint, authen
69
71
  accessToken: data.access_token,
70
72
  refreshToken: data.refresh_token,
71
73
  tokenType: data.token_type,
72
- scopes: data.scope?.split(" "),
74
+ scopes: parseScopeField(data.scope),
73
75
  idToken: data.id_token
74
76
  };
75
77
  if (data.expires_in) {
@@ -83,4 +85,4 @@ async function refreshAccessToken({ refreshToken, options, tokenEndpoint, authen
83
85
  return tokens;
84
86
  }
85
87
  //#endregion
86
- export { createRefreshAccessTokenRequest, refreshAccessToken, refreshAccessTokenRequest };
88
+ export { refreshAccessToken, refreshAccessTokenRequest };
@@ -0,0 +1,65 @@
1
+ import { BetterAuthError } from "../error/index.mjs";
2
+ import { betterFetch } from "@better-fetch/fetch";
3
+ //#region src/oauth2/reject-redirects.ts
4
+ const httpRedirectStatuses = new Set([
5
+ 301,
6
+ 302,
7
+ 303,
8
+ 307,
9
+ 308
10
+ ]);
11
+ /**
12
+ * Whether a response from a `redirect: "manual"` fetch is a redirect.
13
+ *
14
+ * Node/undici exposes the real 3xx status. Spec-compliant runtimes (Cloudflare
15
+ * Workers, Deno, browsers) return an opaque-redirect filtered response with
16
+ * status 0 and type `"opaqueredirect"`, so the status alone is not enough.
17
+ */
18
+ function isRedirectResponse(response) {
19
+ return response.type === "opaqueredirect" || httpRedirectStatuses.has(response.status);
20
+ }
21
+ function redirectRefused(endpoint) {
22
+ return new BetterAuthError(`The OAuth endpoint "${endpoint}" returned an HTTP redirect. Server-side OAuth fetches refuse redirects to prevent SSRF; configure the final endpoint URL.`);
23
+ }
24
+ /**
25
+ * Fetch option that refuses HTTP redirects portably.
26
+ *
27
+ * Cloudflare Workers (workerd) rejects `redirect: "error"`, so manual mode is
28
+ * used and the resolved response is checked with {@link assertResponseNotRedirect}
29
+ * (or, for betterFetch, with {@link fetchRefusingRedirects}).
30
+ */
31
+ const noFollowRedirect = { redirect: "manual" };
32
+ /**
33
+ * Throw when a native-`fetch` response (e.g. jose's JWKS loader) resolved to a
34
+ * redirect, so an attacker-influenced endpoint cannot bounce a server-side
35
+ * request to an internal address.
36
+ */
37
+ function assertResponseNotRedirect(endpoint, response) {
38
+ if (isRedirectResponse(response)) throw redirectRefused(endpoint);
39
+ }
40
+ /**
41
+ * betterFetch that refuses HTTP redirects on a server-side OAuth fetch.
42
+ *
43
+ * Returns the betterFetch result and throws if the endpoint redirected, on both
44
+ * undici (real 3xx status) and spec-compliant runtimes (opaque redirect, where
45
+ * the error status is 0). The redirect is never followed on any runtime.
46
+ */
47
+ async function fetchRefusingRedirects(url, options) {
48
+ let redirected = false;
49
+ const onError = options?.onError;
50
+ const result = await betterFetch(url, {
51
+ ...options,
52
+ ...noFollowRedirect,
53
+ async onError(context) {
54
+ if (isRedirectResponse(context.response)) redirected = true;
55
+ await onError?.(context);
56
+ }
57
+ }).catch((error) => {
58
+ if (redirected) throw redirectRefused(url);
59
+ throw error;
60
+ });
61
+ if (redirected) throw redirectRefused(url);
62
+ return result;
63
+ }
64
+ //#endregion
65
+ export { assertResponseNotRedirect, fetchRefusingRedirects, noFollowRedirect };
@@ -0,0 +1,17 @@
1
+ import { ClientAssertionGetter } from "./client-assertion.mjs";
2
+
3
+ //#region src/oauth2/token-endpoint-auth.d.ts
4
+ type TokenEndpointAuth = {
5
+ method: "none";
6
+ } | {
7
+ method: "client_secret_basic";
8
+ } | {
9
+ method: "client_secret_post";
10
+ } | {
11
+ method: "private_key_jwt";
12
+ getClientAssertion: ClientAssertionGetter;
13
+ };
14
+ type TokenEndpointAuthMethod = TokenEndpointAuth["method"];
15
+ type TokenEndpointSecretAuthentication = "basic" | "post";
16
+ //#endregion
17
+ export { TokenEndpointAuth, TokenEndpointAuthMethod, TokenEndpointSecretAuthentication };
@@ -0,0 +1,89 @@
1
+ import { getPrimaryClientId } from "./utils.mjs";
2
+ import { encodeBasicCredentials } from "./basic-credentials.mjs";
3
+ import { resolveClientAssertionParams } from "./client-assertion.mjs";
4
+ //#region src/oauth2/token-endpoint-auth.ts
5
+ function getDefaultTokenEndpointAuth(options, authentication) {
6
+ if (authentication === "basic") return { method: "client_secret_basic" };
7
+ if (options.clientSecret) return { method: "client_secret_post" };
8
+ return { method: "none" };
9
+ }
10
+ function assertNoClientSecret(method, options, body) {
11
+ if (options.clientSecret || body.has("client_secret")) throw new Error(`${method} token endpoint authentication cannot be combined with clientSecret`);
12
+ }
13
+ function setClientId(body, clientId) {
14
+ if (clientId) body.set("client_id", clientId);
15
+ }
16
+ function assertClientSecretConfigured(method, options) {
17
+ if (!options.clientSecret) throw new Error(`${method} token endpoint authentication requires clientSecret`);
18
+ }
19
+ function assertClientIdConfigured(method, clientId) {
20
+ if (!clientId) throw new Error(`${method} token endpoint authentication requires clientId`);
21
+ }
22
+ function setClientSecretPostAuth({ body, options, clientId, requireClientSecret }) {
23
+ if (requireClientSecret) assertClientSecretConfigured("client_secret_post", options);
24
+ if (options.clientSecret) {
25
+ assertClientIdConfigured("client_secret_post", clientId);
26
+ setClientId(body, clientId);
27
+ body.set("client_secret", options.clientSecret);
28
+ }
29
+ }
30
+ function setClientSecretBasicAuth({ headers, options, clientId, body }) {
31
+ if (body.has("client_secret")) throw new Error("client_secret_basic token endpoint authentication cannot be combined with client_secret body parameters");
32
+ assertClientSecretConfigured("client_secret_basic", options);
33
+ assertClientIdConfigured("client_secret_basic", clientId);
34
+ headers.authorization = encodeBasicCredentials(clientId, options.clientSecret);
35
+ }
36
+ function assertCompleteManualClientAssertion(body) {
37
+ if (body.has("client_assertion") !== body.has("client_assertion_type")) throw new Error("client_assertion and client_assertion_type must both be provided");
38
+ }
39
+ async function applyTokenEndpointAuth({ body, headers, options, tokenEndpoint, grantType, tokenEndpointAuth, authentication }) {
40
+ assertCompleteManualClientAssertion(body);
41
+ const clientId = getPrimaryClientId(options.clientId);
42
+ if (body.has("client_assertion")) {
43
+ if (tokenEndpointAuth) throw new Error("client_assertion body parameters cannot be combined with tokenEndpointAuth");
44
+ assertNoClientSecret("private_key_jwt", options, body);
45
+ setClientId(body, clientId);
46
+ return;
47
+ }
48
+ const auth = tokenEndpointAuth ?? getDefaultTokenEndpointAuth(options, authentication);
49
+ if (auth.method === "private_key_jwt") {
50
+ assertNoClientSecret(auth.method, options, body);
51
+ assertClientIdConfigured(auth.method, clientId);
52
+ if (!tokenEndpoint) throw new Error("private_key_jwt token endpoint authentication requires tokenEndpoint");
53
+ const assertionParams = await resolveClientAssertionParams({
54
+ getClientAssertion: auth.getClientAssertion,
55
+ context: {
56
+ clientId,
57
+ tokenEndpoint,
58
+ grantType
59
+ }
60
+ });
61
+ setClientId(body, clientId);
62
+ for (const [key, value] of Object.entries(assertionParams)) body.set(key, value);
63
+ return;
64
+ }
65
+ if (auth.method === "none") {
66
+ assertNoClientSecret(auth.method, options, body);
67
+ if (grantType === "client_credentials") throw new Error("none token endpoint authentication cannot be used with client_credentials grant");
68
+ assertClientIdConfigured(auth.method, clientId);
69
+ setClientId(body, clientId);
70
+ return;
71
+ }
72
+ if (auth.method === "client_secret_basic") {
73
+ setClientSecretBasicAuth({
74
+ headers,
75
+ options,
76
+ clientId,
77
+ body
78
+ });
79
+ return;
80
+ }
81
+ setClientSecretPostAuth({
82
+ body,
83
+ options,
84
+ clientId,
85
+ requireClientSecret: tokenEndpointAuth?.method === "client_secret_post"
86
+ });
87
+ }
88
+ //#endregion
89
+ export { applyTokenEndpointAuth };
@@ -2,6 +2,28 @@ import { OAuth2Tokens } from "./oauth-provider.mjs";
2
2
 
3
3
  //#region src/oauth2/utils.d.ts
4
4
  declare function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens;
5
+ /**
6
+ * Fill in `accessTokenExpiresAt` from the provider's configured
7
+ * `accessTokenExpiresIn` when the token response omitted `expires_in`. Without a
8
+ * known expiry, `getAccessToken` cannot tell the token is expired and never
9
+ * refreshes it. No-op when the provider already supplied an expiry or no
10
+ * fallback is configured.
11
+ */
12
+ declare function applyDefaultAccessTokenExpiry(tokens: OAuth2Tokens, accessTokenExpiresIn: number | undefined): OAuth2Tokens;
13
+ /**
14
+ * Compute the union of stored and incoming OAuth scopes, preserving
15
+ * stored insertion order and dropping duplicates.
16
+ */
17
+ declare function mergeScopes(stored: string | null | undefined, incoming: string[] | undefined): string;
18
+ /**
19
+ * Return the provider's primary Client ID: the single string, or the entry at
20
+ * array index 0 for the cross-platform form used by ID token audience
21
+ * verification. Index 0 is the designated primary and pairs with
22
+ * `clientSecret` for the authorization code flow; later array entries are
23
+ * only used as additional accepted audiences. Returns `undefined` when the
24
+ * primary value is missing or an empty string.
25
+ */
26
+ declare function getPrimaryClientId(clientId: unknown): string | undefined;
5
27
  declare function generateCodeChallenge(codeVerifier: string): Promise<string>;
6
28
  //#endregion
7
- export { generateCodeChallenge, getOAuth2Tokens };
29
+ export { applyDefaultAccessTokenExpiry, generateCodeChallenge, getOAuth2Tokens, getPrimaryClientId, mergeScopes };
@@ -1,5 +1,19 @@
1
1
  import { base64Url } from "@better-auth/utils/base64";
2
2
  //#region src/oauth2/utils.ts
3
+ /**
4
+ * Parse a provider's `scope` token-response field into a string array.
5
+ *
6
+ * RFC 6749 Section 3.3 defines `scope` as a space-delimited string, but
7
+ * providers vary: some return an already-split array. Accept both forms and
8
+ * drop empty or non-string entries.
9
+ *
10
+ * @see https://github.com/better-auth/better-auth/issues/9076
11
+ */
12
+ function parseScopeField(scope) {
13
+ if (Array.isArray(scope)) return scope.map((s) => typeof s === "string" ? s.trim() : "").filter(Boolean);
14
+ if (typeof scope === "string") return scope.trim().split(/\s+/).filter(Boolean);
15
+ return [];
16
+ }
3
17
  function getOAuth2Tokens(data) {
4
18
  const getDate = (seconds) => {
5
19
  const now = /* @__PURE__ */ new Date();
@@ -11,15 +25,47 @@ function getOAuth2Tokens(data) {
11
25
  refreshToken: data.refresh_token,
12
26
  accessTokenExpiresAt: data.expires_in ? getDate(data.expires_in) : void 0,
13
27
  refreshTokenExpiresAt: data.refresh_token_expires_in ? getDate(data.refresh_token_expires_in) : void 0,
14
- scopes: data?.scope ? typeof data.scope === "string" ? data.scope.split(" ") : data.scope : [],
28
+ scopes: parseScopeField(data.scope),
15
29
  idToken: data.id_token,
16
30
  raw: data
17
31
  };
18
32
  }
33
+ /**
34
+ * Fill in `accessTokenExpiresAt` from the provider's configured
35
+ * `accessTokenExpiresIn` when the token response omitted `expires_in`. Without a
36
+ * known expiry, `getAccessToken` cannot tell the token is expired and never
37
+ * refreshes it. No-op when the provider already supplied an expiry or no
38
+ * fallback is configured.
39
+ */
40
+ function applyDefaultAccessTokenExpiry(tokens, accessTokenExpiresIn) {
41
+ if (!tokens.accessTokenExpiresAt && accessTokenExpiresIn) tokens.accessTokenExpiresAt = new Date(Date.now() + accessTokenExpiresIn * 1e3);
42
+ return tokens;
43
+ }
44
+ /**
45
+ * Compute the union of stored and incoming OAuth scopes, preserving
46
+ * stored insertion order and dropping duplicates.
47
+ */
48
+ function mergeScopes(stored, incoming) {
49
+ const existing = stored ? stored.split(",").map((scope) => scope.trim()).filter(Boolean) : [];
50
+ const next = (incoming ?? []).map((scope) => scope.trim()).filter(Boolean);
51
+ return [...new Set([...existing, ...next])].join(",");
52
+ }
53
+ /**
54
+ * Return the provider's primary Client ID: the single string, or the entry at
55
+ * array index 0 for the cross-platform form used by ID token audience
56
+ * verification. Index 0 is the designated primary and pairs with
57
+ * `clientSecret` for the authorization code flow; later array entries are
58
+ * only used as additional accepted audiences. Returns `undefined` when the
59
+ * primary value is missing or an empty string.
60
+ */
61
+ function getPrimaryClientId(clientId) {
62
+ const value = Array.isArray(clientId) ? clientId[0] : clientId;
63
+ return typeof value === "string" && value.length > 0 ? value : void 0;
64
+ }
19
65
  async function generateCodeChallenge(codeVerifier) {
20
66
  const data = new TextEncoder().encode(codeVerifier);
21
67
  const hash = await crypto.subtle.digest("SHA-256", data);
22
68
  return base64Url.encode(new Uint8Array(hash), { padding: false });
23
69
  }
24
70
  //#endregion
25
- export { generateCodeChallenge, getOAuth2Tokens };
71
+ export { applyDefaultAccessTokenExpiry, generateCodeChallenge, getOAuth2Tokens, getPrimaryClientId, mergeScopes, parseScopeField };
@@ -1,64 +1,41 @@
1
- import { ClientAssertionConfig } from "./client-assertion.mjs";
2
1
  import { AwaitableFunction } from "../types/helper.mjs";
3
2
  import { OAuth2Tokens, ProviderOptions } from "./oauth-provider.mjs";
3
+ import { TokenEndpointAuth, TokenEndpointSecretAuthentication } from "./token-endpoint-auth.mjs";
4
4
  import * as jose from "jose";
5
5
 
6
6
  //#region src/oauth2/validate-authorization-code.d.ts
7
- declare function authorizationCodeRequest({
8
- code,
9
- codeVerifier,
10
- redirectURI,
11
- options,
12
- authentication,
13
- clientAssertion,
14
- tokenEndpoint,
15
- deviceId,
16
- headers,
17
- additionalParams,
18
- resource
19
- }: {
7
+ interface AuthorizationCodeRequestInput {
20
8
  code: string;
21
9
  redirectURI: string;
22
10
  options: AwaitableFunction<Partial<ProviderOptions>>;
23
11
  codeVerifier?: string | undefined;
24
12
  deviceId?: string | undefined;
25
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
26
- clientAssertion?: ClientAssertionConfig | undefined; /** Token endpoint URL. Used as the JWT `aud` claim when signing assertions. */
13
+ authentication?: TokenEndpointSecretAuthentication | undefined;
14
+ tokenEndpointAuth?: TokenEndpointAuth | undefined;
27
15
  tokenEndpoint?: string | undefined;
28
16
  headers?: Record<string, string> | undefined;
29
17
  additionalParams?: Record<string, string> | undefined;
30
18
  resource?: (string | string[]) | undefined;
31
- }): Promise<{
32
- body: URLSearchParams;
33
- headers: Record<string, any>;
34
- }>;
35
- /**
36
- * @deprecated use async'd authorizationCodeRequest instead
37
- */
38
- declare function createAuthorizationCodeRequest({
19
+ }
20
+ interface ValidateAuthorizationCodeInput extends AuthorizationCodeRequestInput {
21
+ tokenEndpoint: string;
22
+ }
23
+ declare function authorizationCodeRequest({
39
24
  code,
40
25
  codeVerifier,
41
26
  redirectURI,
42
27
  options,
43
28
  authentication,
29
+ tokenEndpointAuth,
30
+ tokenEndpoint,
44
31
  deviceId,
45
32
  headers,
46
33
  additionalParams,
47
34
  resource
48
- }: {
49
- code: string;
50
- redirectURI: string;
51
- options: Partial<ProviderOptions>;
52
- codeVerifier?: string | undefined;
53
- deviceId?: string | undefined;
54
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
55
- headers?: Record<string, string> | undefined;
56
- additionalParams?: Record<string, string> | undefined;
57
- resource?: (string | string[]) | undefined;
58
- }): {
35
+ }: AuthorizationCodeRequestInput): Promise<{
59
36
  body: URLSearchParams;
60
- headers: Record<string, any>;
61
- };
37
+ headers: Record<string, string>;
38
+ }>;
62
39
  declare function validateAuthorizationCode({
63
40
  code,
64
41
  codeVerifier,
@@ -66,27 +43,15 @@ declare function validateAuthorizationCode({
66
43
  options,
67
44
  tokenEndpoint,
68
45
  authentication,
69
- clientAssertion,
46
+ tokenEndpointAuth,
70
47
  deviceId,
71
48
  headers,
72
49
  additionalParams,
73
50
  resource
74
- }: {
75
- code: string;
76
- redirectURI: string;
77
- options: AwaitableFunction<Partial<ProviderOptions>>;
78
- codeVerifier?: string | undefined;
79
- deviceId?: string | undefined;
80
- tokenEndpoint: string;
81
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
82
- clientAssertion?: ClientAssertionConfig | undefined;
83
- headers?: Record<string, string> | undefined;
84
- additionalParams?: Record<string, string> | undefined;
85
- resource?: (string | string[]) | undefined;
86
- }): Promise<OAuth2Tokens>;
51
+ }: ValidateAuthorizationCodeInput): Promise<OAuth2Tokens>;
87
52
  declare function validateToken(token: string, jwksEndpoint: string, options?: {
88
53
  audience?: string | string[];
89
54
  issuer?: string | string[];
90
55
  }): Promise<jose.JWTVerifyResult<jose.JWTPayload> & jose.ResolvedKey>;
91
56
  //#endregion
92
- export { authorizationCodeRequest, createAuthorizationCodeRequest, validateAuthorizationCode, validateToken };
57
+ export { authorizationCodeRequest, validateAuthorizationCode, validateToken };