@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,5 +1,59 @@
1
+ import type { JWTVerifyGetKey } from "jose";
1
2
  import type { Awaitable, LiteralString } from "../types";
2
3
 
4
+ /**
5
+ * id_token verification config for a social provider.
6
+ *
7
+ * Declares how a client-submitted id_token is verified. The shared verifier
8
+ * (`verifyProviderIdToken`) consumes this instead of each provider implementing its own
9
+ * boolean check, so verification is centralized and fail-closed: a provider without a config
10
+ * cannot accept a forged token by omission.
11
+ */
12
+ export type OAuthIdTokenConfig =
13
+ | {
14
+ /**
15
+ * JWKS resolver used to verify the JWS signature. Accepts a jose
16
+ * `createRemoteJWKSet` resolver or a key-resolving function
17
+ * `(protectedHeader) => key`.
18
+ */
19
+ jwks: JWTVerifyGetKey;
20
+ /** Expected `iss`. Omit for providers whose issuer varies per tenant. */
21
+ issuer?: (string | string[]) | undefined;
22
+ /** Expected `aud`, usually the client ID. */
23
+ audience: string | string[];
24
+ /** Permitted JWS algorithms. Defaults to the token's `alg` header. */
25
+ algorithms?: string[] | undefined;
26
+ /** Maximum token age passed to jose (e.g. `"1h"`). */
27
+ maxTokenAge?: string | undefined;
28
+ /**
29
+ * How the `nonce` claim is compared to the expected nonce.
30
+ * - `"exact"` (default): strict equality.
31
+ * - `"exact-or-sha256"`: matches the raw nonce or its SHA-256 hex digest (Apple).
32
+ */
33
+ nonceComparison?: ("exact" | "exact-or-sha256") | undefined;
34
+ /**
35
+ * Accept non-JWS (opaque) tokens without signature verification. Identity is then
36
+ * resolved by getUserInfo from the access token via the provider userinfo endpoint,
37
+ * which validates it (e.g. Facebook Graph access tokens).
38
+ */
39
+ allowOpaqueToken?: boolean | undefined;
40
+ /**
41
+ * Provider-specific claim check applied after the signature, issuer,
42
+ * audience, max-age, and nonce checks pass. Return `false` to reject the
43
+ * token. Used to enforce constraints the standard checks cannot express,
44
+ * e.g. Google's hosted-domain (`hd`) restriction. Omitted by providers
45
+ * that have no extra claim requirement.
46
+ */
47
+ verifyClaims?: ((claims: Record<string, unknown>) => boolean) | undefined;
48
+ }
49
+ | {
50
+ /**
51
+ * Custom verifier for providers that cannot verify against a local JWKS, such as a
52
+ * remote verification endpoint (e.g. LINE).
53
+ */
54
+ verify: (token: string, nonce?: string) => Promise<boolean>;
55
+ };
56
+
3
57
  export interface OAuth2Tokens {
4
58
  tokenType?: string | undefined;
5
59
  accessToken?: string | undefined;
@@ -23,11 +77,34 @@ export type OAuth2UserInfo = {
23
77
  emailVerified: boolean;
24
78
  };
25
79
 
80
+ /**
81
+ * Request metadata available to provider refresh hooks.
82
+ *
83
+ * The refresh flow may be triggered by endpoints such as `getAccessToken` or
84
+ * `refreshToken`; this context gives provider hooks access to the triggering
85
+ * request without exposing the full endpoint implementation surface.
86
+ */
87
+ export interface OAuthRefreshContext {
88
+ headers?: Headers | undefined;
89
+ request?: Request | undefined;
90
+ }
91
+
26
92
  export interface OAuthProvider<
27
93
  T extends Record<string, any> = Record<string, any>,
28
94
  O extends Record<string, any> = Partial<ProviderOptions>,
29
95
  > {
30
96
  id: LiteralString;
97
+ /**
98
+ * Optional path under the resolved per-request `baseURL` where this
99
+ * provider's OAuth callback handler is mounted. Providers that use the
100
+ * shared `/callback/<id>` route can omit this.
101
+ *
102
+ * Custom paths must start with `/`.
103
+ *
104
+ * Endpoints compose `redirectURI = ctx.context.baseURL + callbackPath` per
105
+ * request, so the provider must not hardcode an origin or `baseURL` here.
106
+ */
107
+ callbackPath?: string | undefined;
31
108
  createAuthorizationURL: (data: {
32
109
  state: string;
33
110
  codeVerifier: string;
@@ -35,6 +112,19 @@ export interface OAuthProvider<
35
112
  redirectURI: string;
36
113
  display?: string | undefined;
37
114
  loginHint?: string | undefined;
115
+ /**
116
+ * OIDC nonce generated by the redirect initiator and persisted in OAuth
117
+ * state. Providers that set `requiresIdTokenNonce` must forward this to
118
+ * the authorization URL as the `nonce` parameter.
119
+ */
120
+ idTokenNonce?: string | undefined;
121
+ /**
122
+ * Extra query parameters to append to the authorization URL.
123
+ * Providers forward these to the shared `createAuthorizationURL` helper,
124
+ * which drops any keys present in `RESERVED_AUTHORIZATION_PARAMS`
125
+ * before applying them.
126
+ */
127
+ additionalParams?: Record<string, string> | undefined;
38
128
  }) => Awaitable<URL>;
39
129
  name: string;
40
130
  validateAuthorizationCode: (data: {
@@ -45,6 +135,12 @@ export interface OAuthProvider<
45
135
  }) => Promise<OAuth2Tokens | null>;
46
136
  getUserInfo: (
47
137
  token: OAuth2Tokens & {
138
+ /**
139
+ * OIDC nonce recovered from OAuth state. Providers that required an
140
+ * ID-token nonce must pass this to `verifyProviderIdToken` before
141
+ * trusting ID-token claims.
142
+ */
143
+ expectedIdTokenNonce?: string | undefined;
48
144
  /**
49
145
  * The user object from the provider
50
146
  * This is only available for some providers like Apple
@@ -64,27 +160,38 @@ export interface OAuthProvider<
64
160
  data: T;
65
161
  } | null>;
66
162
  /**
67
- * Custom function to refresh a token
163
+ * Custom function to refresh a token.
164
+ *
165
+ * Receives request metadata from the endpoint that triggered the refresh.
166
+ * Providers that don't need request-scoped data can ignore the second
167
+ * argument.
68
168
  */
69
169
  refreshAccessToken?:
70
- | ((refreshToken: string) => Promise<OAuth2Tokens>)
170
+ | ((
171
+ refreshToken: string,
172
+ ctx?: OAuthRefreshContext,
173
+ ) => Promise<OAuth2Tokens>)
71
174
  | undefined;
72
175
  revokeToken?: ((token: string) => Promise<void>) | undefined;
73
176
  /**
74
- * Verify the id token
75
- * @param token - The id token
76
- * @param nonce - The nonce
77
- * @returns True if the id token is valid, false otherwise
177
+ * Declarative id_token verification config consumed by the shared
178
+ * `verifyProviderIdToken` verifier. Providers set this instead of implementing a boolean
179
+ * verify method, which keeps verification centralized and fail-closed.
78
180
  */
79
- verifyIdToken?:
80
- | ((token: string, nonce?: string) => Promise<boolean>)
81
- | undefined;
181
+ idToken?: OAuthIdTokenConfig | undefined;
82
182
  /**
83
183
  * The expected issuer identifier for this provider (RFC 9207).
84
184
  * When set, the callback handler validates the `iss` query parameter
85
185
  * against this value to prevent authorization server mix-up attacks.
86
186
  */
87
187
  issuer?: string | undefined;
188
+ /**
189
+ * Require shared OAuth redirect routes to bind ID-token verification to an
190
+ * authorization request nonce. When true, routes generate `idTokenNonce`,
191
+ * pass it to `createAuthorizationURL`, persist it in state, and provide it
192
+ * back to `getUserInfo` as `expectedIdTokenNonce`.
193
+ */
194
+ requiresIdTokenNonce?: boolean | undefined;
88
195
  /**
89
196
  * Disable implicit sign up for new users. When set to true for the provider,
90
197
  * sign-in need to be called with with requestSignUp as true to create new users.
@@ -94,6 +201,17 @@ export interface OAuthProvider<
94
201
  * Disable sign up for new users.
95
202
  */
96
203
  disableSignUp?: boolean | undefined;
204
+ /**
205
+ * Accept callbacks that arrive without a `state` parameter. When true,
206
+ * the shared OAuth callback handler restarts the flow server-side with
207
+ * fresh `state` and PKCE instead of rejecting the request. Intended for
208
+ * providers that initiate OAuth without RP-side flow kickoff (e.g.
209
+ * Clever). Leave unset for any provider that always initiates from the
210
+ * RP.
211
+ *
212
+ * @default false
213
+ */
214
+ allowIdpInitiated?: boolean | undefined;
97
215
  /**
98
216
  * Options for the provider
99
217
  */
@@ -104,9 +222,10 @@ export type ProviderOptions<Profile extends Record<string, any> = any> = {
104
222
  /**
105
223
  * The client ID of your application.
106
224
  *
107
- * This is usually a string but can be any type depending on the provider.
225
+ * Some providers accept multiple platform client IDs. The first entry is the
226
+ * primary client ID used for token endpoint client authentication.
108
227
  */
109
- clientId?: unknown | undefined;
228
+ clientId?: LiteralString | string[] | undefined;
110
229
  /**
111
230
  * The client secret of your application
112
231
  */
@@ -225,4 +344,27 @@ export type ProviderOptions<Profile extends Record<string, any> = any> = {
225
344
  * @default false
226
345
  */
227
346
  overrideUserInfoOnSignIn?: boolean | undefined;
347
+ /**
348
+ * Require this provider's email to be verified before a session is created.
349
+ *
350
+ * When the provider reports the email as unverified, the user and account are
351
+ * still created/linked, but no session is issued: the OAuth callback redirects
352
+ * with `?error=email_not_verified` and id-token sign-in returns a `403`
353
+ * `EMAIL_NOT_VERIFIED`. A verification email is (re)sent per the
354
+ * `emailVerification` settings (`sendOnSignUp` / `sendOnSignIn`).
355
+ *
356
+ * The gate checks the local user's verification state, not the provider's
357
+ * claim on each request: a user already verified through another method (or a
358
+ * prior verified sign-in) keeps access even if the provider later reports the
359
+ * email as unverified.
360
+ *
361
+ * This is opt-in per provider and is independent of
362
+ * `emailAndPassword.requireEmailVerification`; enabling that does not gate
363
+ * social sign-in. Only enable it for providers that report a trustworthy
364
+ * `email_verified` signal: several providers always report the email as
365
+ * unverified, which would block every sign-in.
366
+ *
367
+ * @default false
368
+ */
369
+ requireEmailVerification?: boolean | undefined;
228
370
  };
@@ -1,99 +1,105 @@
1
- import { base64 } from "@better-auth/utils/base64";
2
- import { betterFetch } from "@better-fetch/fetch";
3
1
  import type { AwaitableFunction } from "../types";
4
- import type { ClientAssertionConfig } from "./client-assertion";
5
- import { resolveAssertionParams } from "./client-assertion";
6
2
  import type { OAuth2Tokens, ProviderOptions } from "./oauth-provider";
3
+ import { fetchRefusingRedirects } from "./reject-redirects";
4
+ import type {
5
+ TokenEndpointAuth,
6
+ TokenEndpointSecretAuthentication,
7
+ } from "./token-endpoint-auth";
8
+ import { applyTokenEndpointAuth } from "./token-endpoint-auth";
9
+ import { parseScopeField } from "./utils";
10
+
11
+ interface RefreshAccessTokenRequestInput {
12
+ refreshToken: string;
13
+ options: AwaitableFunction<Partial<ProviderOptions>>;
14
+ authentication?: TokenEndpointSecretAuthentication | undefined;
15
+ tokenEndpointAuth?: TokenEndpointAuth | undefined;
16
+ tokenEndpoint?: string | undefined;
17
+ extraParams?: Record<string, string> | undefined;
18
+ resource?: (string | string[]) | undefined;
19
+ }
20
+
21
+ interface RefreshAccessTokenRequestBaseInput {
22
+ refreshToken: string;
23
+ options: ProviderOptions;
24
+ extraParams?: Record<string, string> | undefined;
25
+ resource?: (string | string[]) | undefined;
26
+ }
27
+
28
+ interface RefreshAccessTokenInput extends RefreshAccessTokenRequestInput {
29
+ options: Partial<ProviderOptions>;
30
+ tokenEndpoint: string;
31
+ }
32
+
33
+ /**
34
+ * Body keys owned by the refresh-token flow or unsafe to copy from caller input.
35
+ */
36
+ const BLOCKED_REFRESH_TOKEN_PARAMS = [
37
+ "grant_type",
38
+ "refresh_token",
39
+ "__proto__",
40
+ "constructor",
41
+ "prototype",
42
+ ] as const;
43
+
44
+ const BLOCKED_REFRESH_TOKEN_PARAMS_SET: ReadonlySet<string> = new Set(
45
+ BLOCKED_REFRESH_TOKEN_PARAMS,
46
+ );
7
47
 
8
48
  export async function refreshAccessTokenRequest({
9
49
  refreshToken,
10
50
  options,
11
51
  authentication,
12
- clientAssertion,
52
+ tokenEndpointAuth,
13
53
  tokenEndpoint,
14
54
  extraParams,
15
55
  resource,
16
- }: {
17
- refreshToken: string;
18
- options: AwaitableFunction<Partial<ProviderOptions>>;
19
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
20
- clientAssertion?: ClientAssertionConfig | undefined;
21
- /** Token endpoint URL. Used as the JWT `aud` claim when signing assertions. */
22
- tokenEndpoint?: string | undefined;
23
- extraParams?: Record<string, string> | undefined;
24
- resource?: (string | string[]) | undefined;
25
- }) {
56
+ }: RefreshAccessTokenRequestInput) {
26
57
  options = typeof options === "function" ? await options() : options;
27
-
28
- if (authentication === "private_key_jwt") {
29
- if (!clientAssertion) {
30
- throw new Error(
31
- "private_key_jwt authentication requires a clientAssertion configuration",
32
- );
33
- }
34
- const primaryClientId = Array.isArray(options.clientId)
35
- ? options.clientId[0]
36
- : options.clientId;
37
- const assertionParams = await resolveAssertionParams({
38
- clientAssertion,
39
- clientId: primaryClientId,
40
- tokenEndpoint,
41
- });
42
- extraParams = { ...extraParams, ...assertionParams };
43
- }
44
-
45
- return createRefreshAccessTokenRequest({
58
+ const request = buildRefreshAccessTokenRequest({
46
59
  refreshToken,
47
60
  options,
48
- authentication,
49
61
  extraParams,
50
62
  resource,
51
63
  });
64
+
65
+ await applyTokenEndpointAuth({
66
+ body: request.body,
67
+ headers: request.headers,
68
+ options,
69
+ tokenEndpoint: tokenEndpoint ?? "",
70
+ grantType: "refresh_token",
71
+ tokenEndpointAuth,
72
+ authentication,
73
+ });
74
+
75
+ return request;
52
76
  }
53
77
 
54
- /**
55
- * @deprecated use async'd refreshAccessTokenRequest instead
56
- */
57
- export function createRefreshAccessTokenRequest({
78
+ function applyRefreshExtraParams(
79
+ body: URLSearchParams,
80
+ extraParams: Record<string, string> | undefined,
81
+ ) {
82
+ if (!extraParams) return;
83
+ for (const [key, value] of Object.entries(extraParams)) {
84
+ if (BLOCKED_REFRESH_TOKEN_PARAMS_SET.has(key)) continue;
85
+ body.set(key, value);
86
+ }
87
+ }
88
+
89
+ function buildRefreshAccessTokenRequest({
58
90
  refreshToken,
59
91
  options,
60
- authentication,
61
92
  extraParams,
62
93
  resource,
63
- }: {
64
- refreshToken: string;
65
- options: ProviderOptions;
66
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
67
- extraParams?: Record<string, string> | undefined;
68
- resource?: (string | string[]) | undefined;
69
- }) {
94
+ }: RefreshAccessTokenRequestBaseInput) {
70
95
  const body = new URLSearchParams();
71
- const headers: Record<string, any> = {
96
+ const headers: Record<string, string> = {
72
97
  "content-type": "application/x-www-form-urlencoded",
73
98
  accept: "application/json",
74
99
  };
75
100
 
76
101
  body.set("grant_type", "refresh_token");
77
102
  body.set("refresh_token", refreshToken);
78
- const primaryClientId = Array.isArray(options.clientId)
79
- ? options.clientId[0]
80
- : options.clientId;
81
- if (authentication === "basic") {
82
- if (primaryClientId) {
83
- headers["authorization"] =
84
- "Basic " +
85
- base64.encode(`${primaryClientId}:${options.clientSecret ?? ""}`);
86
- } else {
87
- headers["authorization"] =
88
- "Basic " + base64.encode(`:${options.clientSecret ?? ""}`);
89
- }
90
- } else {
91
- body.set("client_id", primaryClientId);
92
- if (authentication !== "private_key_jwt" && options.clientSecret) {
93
- body.set("client_secret", options.clientSecret);
94
- }
95
- }
96
-
97
103
  if (resource) {
98
104
  if (typeof resource === "string") {
99
105
  body.append("resource", resource);
@@ -104,9 +110,7 @@ export function createRefreshAccessTokenRequest({
104
110
  }
105
111
  }
106
112
  if (extraParams) {
107
- for (const [key, value] of Object.entries(extraParams)) {
108
- body.set(key, value);
109
- }
113
+ applyRefreshExtraParams(body, extraParams);
110
114
  }
111
115
 
112
116
  return {
@@ -120,32 +124,27 @@ export async function refreshAccessToken({
120
124
  options,
121
125
  tokenEndpoint,
122
126
  authentication,
123
- clientAssertion,
127
+ tokenEndpointAuth,
124
128
  extraParams,
125
- }: {
126
- refreshToken: string;
127
- options: Partial<ProviderOptions>;
128
- tokenEndpoint: string;
129
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
130
- clientAssertion?: ClientAssertionConfig | undefined;
131
- extraParams?: Record<string, string> | undefined;
132
- }): Promise<OAuth2Tokens> {
129
+ resource,
130
+ }: RefreshAccessTokenInput): Promise<OAuth2Tokens> {
133
131
  const { body, headers } = await refreshAccessTokenRequest({
134
132
  refreshToken,
135
133
  options,
136
134
  authentication,
137
- clientAssertion,
135
+ tokenEndpointAuth,
138
136
  tokenEndpoint,
139
137
  extraParams,
138
+ resource,
140
139
  });
141
140
 
142
- const { data, error } = await betterFetch<{
141
+ const { data, error } = await fetchRefusingRedirects<{
143
142
  access_token: string;
144
143
  refresh_token?: string | undefined;
145
144
  expires_in?: number | undefined;
146
145
  refresh_token_expires_in?: number | undefined;
147
146
  token_type?: string | undefined;
148
- scope?: string | undefined;
147
+ scope?: unknown;
149
148
  id_token?: string | undefined;
150
149
  }>(tokenEndpoint, {
151
150
  method: "POST",
@@ -159,7 +158,7 @@ export async function refreshAccessToken({
159
158
  accessToken: data.access_token,
160
159
  refreshToken: data.refresh_token,
161
160
  tokenType: data.token_type,
162
- scopes: data.scope?.split(" "),
161
+ scopes: parseScopeField(data.scope),
163
162
  idToken: data.id_token,
164
163
  };
165
164
 
@@ -0,0 +1,75 @@
1
+ // cspell:ignore workerd
2
+ import type { BetterFetchOption } from "@better-fetch/fetch";
3
+ import { betterFetch } from "@better-fetch/fetch";
4
+ import { BetterAuthError } from "../error";
5
+
6
+ const httpRedirectStatuses = new Set([301, 302, 303, 307, 308]);
7
+
8
+ /**
9
+ * Whether a response from a `redirect: "manual"` fetch is a redirect.
10
+ *
11
+ * Node/undici exposes the real 3xx status. Spec-compliant runtimes (Cloudflare
12
+ * Workers, Deno, browsers) return an opaque-redirect filtered response with
13
+ * status 0 and type `"opaqueredirect"`, so the status alone is not enough.
14
+ */
15
+ function isRedirectResponse(response: Response): boolean {
16
+ return (
17
+ response.type === "opaqueredirect" ||
18
+ httpRedirectStatuses.has(response.status)
19
+ );
20
+ }
21
+
22
+ function redirectRefused(endpoint: string): BetterAuthError {
23
+ return new BetterAuthError(
24
+ `The OAuth endpoint "${endpoint}" returned an HTTP redirect. Server-side OAuth fetches refuse redirects to prevent SSRF; configure the final endpoint URL.`,
25
+ );
26
+ }
27
+
28
+ /**
29
+ * Fetch option that refuses HTTP redirects portably.
30
+ *
31
+ * Cloudflare Workers (workerd) rejects `redirect: "error"`, so manual mode is
32
+ * used and the resolved response is checked with {@link assertResponseNotRedirect}
33
+ * (or, for betterFetch, with {@link fetchRefusingRedirects}).
34
+ */
35
+ export const noFollowRedirect = { redirect: "manual" } as const;
36
+
37
+ /**
38
+ * Throw when a native-`fetch` response (e.g. jose's JWKS loader) resolved to a
39
+ * redirect, so an attacker-influenced endpoint cannot bounce a server-side
40
+ * request to an internal address.
41
+ */
42
+ export function assertResponseNotRedirect(
43
+ endpoint: string,
44
+ response: Response,
45
+ ): void {
46
+ if (isRedirectResponse(response)) throw redirectRefused(endpoint);
47
+ }
48
+
49
+ /**
50
+ * betterFetch that refuses HTTP redirects on a server-side OAuth fetch.
51
+ *
52
+ * Returns the betterFetch result and throws if the endpoint redirected, on both
53
+ * undici (real 3xx status) and spec-compliant runtimes (opaque redirect, where
54
+ * the error status is 0). The redirect is never followed on any runtime.
55
+ */
56
+ export async function fetchRefusingRedirects<T>(
57
+ url: string,
58
+ options?: BetterFetchOption,
59
+ ) {
60
+ let redirected = false;
61
+ const onError = options?.onError;
62
+ const result = await betterFetch<T>(url, {
63
+ ...options,
64
+ ...noFollowRedirect,
65
+ async onError(context) {
66
+ if (isRedirectResponse(context.response)) redirected = true;
67
+ await onError?.(context);
68
+ },
69
+ }).catch((error) => {
70
+ if (redirected) throw redirectRefused(url);
71
+ throw error;
72
+ });
73
+ if (redirected) throw redirectRefused(url);
74
+ return result;
75
+ }