@better-auth/core 1.7.0-beta.3 → 1.7.0-beta.5

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 (188) hide show
  1. package/dist/api/index.d.mts +3 -3
  2. package/dist/context/global.mjs +1 -1
  3. package/dist/db/adapter/factory.mjs +62 -0
  4. package/dist/db/adapter/index.d.mts +35 -1
  5. package/dist/db/adapter/types.d.mts +1 -1
  6. package/dist/db/get-tables.mjs +3 -3
  7. package/dist/db/schema/account.d.mts +1 -1
  8. package/dist/db/schema/account.mjs +1 -1
  9. package/dist/db/type.d.mts +12 -0
  10. package/dist/env/env-impl.mjs +1 -1
  11. package/dist/error/codes.d.mts +6 -0
  12. package/dist/error/codes.mjs +6 -0
  13. package/dist/index.d.mts +2 -2
  14. package/dist/instrumentation/tracer.mjs +1 -1
  15. package/dist/oauth2/authorization-params.d.mts +12 -0
  16. package/dist/oauth2/authorization-params.mjs +12 -0
  17. package/dist/oauth2/basic-credentials.d.mts +30 -0
  18. package/dist/oauth2/basic-credentials.mjs +64 -0
  19. package/dist/oauth2/client-assertion.d.mts +38 -22
  20. package/dist/oauth2/client-assertion.mjs +63 -28
  21. package/dist/oauth2/client-credentials-token.d.mts +19 -40
  22. package/dist/oauth2/client-credentials-token.mjs +18 -29
  23. package/dist/oauth2/create-authorization-url.d.mts +13 -2
  24. package/dist/oauth2/create-authorization-url.mjs +28 -7
  25. package/dist/oauth2/index.d.mts +13 -8
  26. package/dist/oauth2/index.mjs +11 -7
  27. package/dist/oauth2/oauth-provider.d.mts +149 -11
  28. package/dist/oauth2/refresh-access-token.d.mts +20 -40
  29. package/dist/oauth2/refresh-access-token.mjs +20 -33
  30. package/dist/oauth2/scopes.d.mts +76 -0
  31. package/dist/oauth2/scopes.mjs +96 -0
  32. package/dist/oauth2/token-endpoint-auth.d.mts +17 -0
  33. package/dist/oauth2/token-endpoint-auth.mjs +89 -0
  34. package/dist/oauth2/utils.d.mts +9 -1
  35. package/dist/oauth2/utils.mjs +14 -2
  36. package/dist/oauth2/validate-authorization-code.d.mts +17 -52
  37. package/dist/oauth2/validate-authorization-code.mjs +17 -30
  38. package/dist/oauth2/verify-id-token.d.mts +26 -0
  39. package/dist/oauth2/verify-id-token.mjs +62 -0
  40. package/dist/oauth2/verify.d.mts +14 -0
  41. package/dist/oauth2/verify.mjs +38 -12
  42. package/dist/social-providers/apple.d.mts +18 -20
  43. package/dist/social-providers/apple.mjs +15 -28
  44. package/dist/social-providers/atlassian.d.mts +8 -2
  45. package/dist/social-providers/atlassian.mjs +9 -6
  46. package/dist/social-providers/cognito.d.mts +29 -3
  47. package/dist/social-providers/cognito.mjs +30 -34
  48. package/dist/social-providers/discord.d.mts +8 -2
  49. package/dist/social-providers/discord.mjs +20 -6
  50. package/dist/social-providers/dropbox.d.mts +8 -2
  51. package/dist/social-providers/dropbox.mjs +10 -9
  52. package/dist/social-providers/facebook.d.mts +24 -3
  53. package/dist/social-providers/facebook.mjs +51 -24
  54. package/dist/social-providers/figma.d.mts +8 -2
  55. package/dist/social-providers/figma.mjs +8 -7
  56. package/dist/social-providers/github.d.mts +8 -2
  57. package/dist/social-providers/github.mjs +9 -8
  58. package/dist/social-providers/gitlab.d.mts +8 -2
  59. package/dist/social-providers/gitlab.mjs +8 -7
  60. package/dist/social-providers/google.d.mts +32 -4
  61. package/dist/social-providers/google.mjs +26 -29
  62. package/dist/social-providers/huggingface.d.mts +8 -2
  63. package/dist/social-providers/huggingface.mjs +11 -10
  64. package/dist/social-providers/index.d.mts +322 -75
  65. package/dist/social-providers/kakao.d.mts +8 -2
  66. package/dist/social-providers/kakao.mjs +11 -10
  67. package/dist/social-providers/kick.d.mts +8 -2
  68. package/dist/social-providers/kick.mjs +7 -6
  69. package/dist/social-providers/line.d.mts +11 -3
  70. package/dist/social-providers/line.mjs +14 -15
  71. package/dist/social-providers/linear.d.mts +8 -2
  72. package/dist/social-providers/linear.mjs +7 -6
  73. package/dist/social-providers/linkedin.d.mts +8 -2
  74. package/dist/social-providers/linkedin.mjs +12 -11
  75. package/dist/social-providers/microsoft-entra-id.d.mts +33 -7
  76. package/dist/social-providers/microsoft-entra-id.mjs +28 -38
  77. package/dist/social-providers/naver.d.mts +8 -2
  78. package/dist/social-providers/naver.mjs +7 -6
  79. package/dist/social-providers/notion.d.mts +8 -2
  80. package/dist/social-providers/notion.mjs +9 -6
  81. package/dist/social-providers/paybin.d.mts +8 -2
  82. package/dist/social-providers/paybin.mjs +12 -11
  83. package/dist/social-providers/paypal.d.mts +8 -3
  84. package/dist/social-providers/paypal.mjs +10 -14
  85. package/dist/social-providers/polar.d.mts +8 -2
  86. package/dist/social-providers/polar.mjs +11 -10
  87. package/dist/social-providers/railway.d.mts +8 -2
  88. package/dist/social-providers/railway.mjs +11 -10
  89. package/dist/social-providers/reddit.d.mts +8 -2
  90. package/dist/social-providers/reddit.mjs +11 -9
  91. package/dist/social-providers/roblox.d.mts +8 -2
  92. package/dist/social-providers/roblox.mjs +15 -5
  93. package/dist/social-providers/salesforce.d.mts +8 -2
  94. package/dist/social-providers/salesforce.mjs +11 -10
  95. package/dist/social-providers/slack.d.mts +8 -2
  96. package/dist/social-providers/slack.mjs +18 -15
  97. package/dist/social-providers/spotify.d.mts +8 -2
  98. package/dist/social-providers/spotify.mjs +7 -6
  99. package/dist/social-providers/tiktok.d.mts +8 -2
  100. package/dist/social-providers/tiktok.mjs +21 -5
  101. package/dist/social-providers/twitch.d.mts +8 -2
  102. package/dist/social-providers/twitch.mjs +7 -6
  103. package/dist/social-providers/twitter.d.mts +7 -2
  104. package/dist/social-providers/twitter.mjs +11 -10
  105. package/dist/social-providers/vercel.d.mts +8 -2
  106. package/dist/social-providers/vercel.mjs +7 -9
  107. package/dist/social-providers/vk.d.mts +8 -2
  108. package/dist/social-providers/vk.mjs +7 -6
  109. package/dist/social-providers/wechat.d.mts +8 -2
  110. package/dist/social-providers/wechat.mjs +16 -6
  111. package/dist/social-providers/zoom.d.mts +10 -3
  112. package/dist/social-providers/zoom.mjs +14 -15
  113. package/dist/types/context.d.mts +33 -11
  114. package/dist/types/index.d.mts +1 -1
  115. package/dist/types/init-options.d.mts +121 -6
  116. package/dist/utils/ip.d.mts +5 -4
  117. package/dist/utils/ip.mjs +3 -3
  118. package/dist/utils/redirect-uri.d.mts +20 -0
  119. package/dist/utils/redirect-uri.mjs +48 -0
  120. package/dist/utils/string.d.mts +5 -1
  121. package/dist/utils/string.mjs +20 -1
  122. package/dist/utils/url.d.mts +18 -1
  123. package/dist/utils/url.mjs +30 -1
  124. package/package.json +13 -12
  125. package/src/db/adapter/factory.ts +126 -0
  126. package/src/db/adapter/index.ts +32 -0
  127. package/src/db/adapter/types.ts +1 -0
  128. package/src/db/get-tables.ts +8 -3
  129. package/src/db/schema/account.ts +14 -2
  130. package/src/db/type.ts +12 -0
  131. package/src/env/env-impl.ts +1 -2
  132. package/src/error/codes.ts +6 -0
  133. package/src/oauth2/authorization-params.ts +28 -0
  134. package/src/oauth2/basic-credentials.ts +87 -0
  135. package/src/oauth2/client-assertion.ts +131 -58
  136. package/src/oauth2/client-credentials-token.ts +48 -72
  137. package/src/oauth2/create-authorization-url.ts +30 -8
  138. package/src/oauth2/index.ts +42 -10
  139. package/src/oauth2/oauth-provider.ts +161 -12
  140. package/src/oauth2/refresh-access-token.ts +52 -78
  141. package/src/oauth2/scopes.ts +118 -0
  142. package/src/oauth2/token-endpoint-auth.ts +221 -0
  143. package/src/oauth2/utils.ts +21 -5
  144. package/src/oauth2/validate-authorization-code.ts +55 -85
  145. package/src/oauth2/verify-id-token.ts +111 -0
  146. package/src/oauth2/verify.ts +82 -15
  147. package/src/social-providers/apple.ts +32 -45
  148. package/src/social-providers/atlassian.ts +20 -9
  149. package/src/social-providers/cognito.ts +51 -48
  150. package/src/social-providers/discord.ts +37 -22
  151. package/src/social-providers/dropbox.ts +20 -12
  152. package/src/social-providers/facebook.ts +108 -57
  153. package/src/social-providers/figma.ts +21 -10
  154. package/src/social-providers/github.ts +16 -10
  155. package/src/social-providers/gitlab.ts +16 -8
  156. package/src/social-providers/google.ts +67 -46
  157. package/src/social-providers/huggingface.ts +20 -9
  158. package/src/social-providers/kakao.ts +18 -9
  159. package/src/social-providers/kick.ts +20 -8
  160. package/src/social-providers/line.ts +39 -37
  161. package/src/social-providers/linear.ts +20 -7
  162. package/src/social-providers/linkedin.ts +16 -10
  163. package/src/social-providers/microsoft-entra-id.ts +66 -64
  164. package/src/social-providers/naver.ts +14 -7
  165. package/src/social-providers/notion.ts +20 -7
  166. package/src/social-providers/paybin.ts +16 -11
  167. package/src/social-providers/paypal.ts +12 -25
  168. package/src/social-providers/polar.ts +20 -9
  169. package/src/social-providers/railway.ts +20 -9
  170. package/src/social-providers/reddit.ts +22 -10
  171. package/src/social-providers/roblox.ts +31 -15
  172. package/src/social-providers/salesforce.ts +21 -10
  173. package/src/social-providers/slack.ts +31 -16
  174. package/src/social-providers/spotify.ts +20 -7
  175. package/src/social-providers/tiktok.ts +32 -13
  176. package/src/social-providers/twitch.ts +14 -9
  177. package/src/social-providers/twitter.ts +18 -8
  178. package/src/social-providers/vercel.ts +24 -11
  179. package/src/social-providers/vk.ts +20 -7
  180. package/src/social-providers/wechat.ts +28 -8
  181. package/src/social-providers/zoom.ts +28 -19
  182. package/src/types/context.ts +33 -12
  183. package/src/types/index.ts +7 -0
  184. package/src/types/init-options.ts +148 -5
  185. package/src/utils/ip.ts +12 -13
  186. package/src/utils/redirect-uri.ts +54 -0
  187. package/src/utils/string.ts +37 -0
  188. package/src/utils/url.ts +28 -0
@@ -1,38 +1,66 @@
1
+ export { additionalAuthorizationParamsSchema } from "./authorization-params";
2
+ export {
3
+ decodeBasicCredentials,
4
+ encodeBasicCredentials,
5
+ } from "./basic-credentials";
1
6
  export type {
2
- AssertionSigningAlgorithm,
3
- ClientAssertionConfig,
7
+ ClientAssertionContext,
8
+ ClientAssertionGetter,
9
+ ClientAssertionGrantType,
10
+ PrivateKeyJwtClientAssertionGetterOptions,
11
+ PrivateKeyJwtSigningAlgorithm,
4
12
  } from "./client-assertion";
5
13
  export {
6
- ASSERTION_SIGNING_ALGORITHMS,
7
14
  CLIENT_ASSERTION_TYPE,
8
- resolveAssertionParams,
9
- signClientAssertion,
15
+ createPrivateKeyJwtClientAssertionGetter,
16
+ PRIVATE_KEY_JWT_SIGNING_ALGORITHMS,
17
+ resolveClientAssertionParams,
18
+ signPrivateKeyJwtClientAssertion,
10
19
  } from "./client-assertion";
11
20
  export {
12
21
  clientCredentialsToken,
13
22
  clientCredentialsTokenRequest,
14
- createClientCredentialsTokenRequest,
15
23
  } from "./client-credentials-token";
16
- export { createAuthorizationURL } from "./create-authorization-url";
24
+ export {
25
+ createAuthorizationURL,
26
+ RESERVED_AUTHORIZATION_PARAMS,
27
+ RESERVED_AUTHORIZATION_PARAMS_SET,
28
+ } from "./create-authorization-url";
17
29
  export type {
30
+ AuthorizationURLResult,
31
+ GrantAuthority,
18
32
  OAuth2Tokens,
19
33
  OAuth2UserInfo,
20
- OAuthProvider,
34
+ OAuthIdTokenConfig,
35
+ ProviderGrantAuthority,
21
36
  ProviderOptions,
37
+ UpstreamProvider,
22
38
  } from "./oauth-provider";
23
39
  export {
24
- createRefreshAccessTokenRequest,
25
40
  refreshAccessToken,
26
41
  refreshAccessTokenRequest,
27
42
  } from "./refresh-access-token";
28
43
  export {
44
+ includesGrantedScope,
45
+ normalizeScopes,
46
+ parseScopeField,
47
+ readGrantedScopes,
48
+ resolveRequestedScopes,
49
+ unionGrantedScopes,
50
+ } from "./scopes";
51
+ export type {
52
+ TokenEndpointAuth,
53
+ TokenEndpointAuthMethod,
54
+ TokenEndpointSecretAuthentication,
55
+ } from "./token-endpoint-auth";
56
+ export {
57
+ applyDefaultAccessTokenExpiry,
29
58
  generateCodeChallenge,
30
59
  getOAuth2Tokens,
31
60
  getPrimaryClientId,
32
61
  } from "./utils";
33
62
  export {
34
63
  authorizationCodeRequest,
35
- createAuthorizationCodeRequest,
36
64
  validateAuthorizationCode,
37
65
  validateToken,
38
66
  } from "./validate-authorization-code";
@@ -41,3 +69,7 @@ export {
41
69
  verifyAccessToken,
42
70
  verifyJwsAccessToken,
43
71
  } from "./verify";
72
+ export {
73
+ supportsIdTokenSignIn,
74
+ verifyProviderIdToken,
75
+ } from "./verify-id-token";
@@ -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,64 @@ export type OAuth2UserInfo = {
23
77
  emailVerified: boolean;
24
78
  };
25
79
 
26
- export interface OAuthProvider<
80
+ /**
81
+ * The result of building a provider authorization URL.
82
+ *
83
+ * `requestedScopes` is the effective set of scopes encoded in the URL (the
84
+ * provider's built-in defaults + configured `options.scope` + per-request
85
+ * `scopes`, composed by `resolveRequestedScopes`). Callers persist it so the
86
+ * callback can fall back to the request when the provider omits `scope` from
87
+ * its token response (RFC 6749 §5.1).
88
+ */
89
+ export interface AuthorizationURLResult {
90
+ url: URL;
91
+ requestedScopes: string[];
92
+ }
93
+
94
+ /**
95
+ * How much an RP trusts a provider's echoed token-response `scope` when
96
+ * persisting `account.grantedScopes`.
97
+ *
98
+ * - `"full-grant"`: the echo is the user's complete current grant, so the seam
99
+ * replaces the stored grant with it. This is the only path that may narrow
100
+ * the grant. Declare it only for providers whose token response reports the
101
+ * full combined grant, e.g. Google with `include_granted_scopes`.
102
+ * - `"projection"`: the echo is this request's subset, so the seam unions it
103
+ * onto the stored grant. The safe default for every provider.
104
+ * - `"absent-echo"`: the provider omitted `scope`, so the grant equals what was
105
+ * requested (RFC 6749 §5.1) and the seam unions the requested set. Resolved
106
+ * at runtime by the persistence seam, never declared by a provider.
107
+ *
108
+ * @see https://www.rfc-editor.org/rfc/rfc6749#section-5.1
109
+ */
110
+ export type GrantAuthority = "full-grant" | "projection" | "absent-echo";
111
+
112
+ /**
113
+ * The authority a provider may declare for its own echoed scope. `"absent-echo"`
114
+ * is excluded because it is a runtime condition (an omitted echo), not a
115
+ * provider trait.
116
+ */
117
+ export type ProviderGrantAuthority = Exclude<GrantAuthority, "absent-echo">;
118
+
119
+ export interface UpstreamProvider<
27
120
  T extends Record<string, any> = Record<string, any>,
28
121
  O extends Record<string, any> = Partial<ProviderOptions>,
29
122
  > {
30
123
  id: LiteralString;
124
+ /**
125
+ * The path the provider redirects back to, relative to the app base URL,
126
+ * e.g. `/callback/google`.
127
+ */
128
+ callbackPath: string;
129
+ /**
130
+ * How the persistence seam treats this provider's echoed token-response
131
+ * `scope`. Declare `"full-grant"` only when the echo is the user's complete
132
+ * current grant (e.g. Google with `include_granted_scopes`); otherwise the
133
+ * echo is unioned onto the stored grant.
134
+ *
135
+ * @default "projection"
136
+ */
137
+ grantAuthority?: ProviderGrantAuthority | undefined;
31
138
  createAuthorizationURL: (data: {
32
139
  state: string;
33
140
  codeVerifier: string;
@@ -35,7 +142,14 @@ export interface OAuthProvider<
35
142
  redirectURI: string;
36
143
  display?: string | undefined;
37
144
  loginHint?: string | undefined;
38
- }) => Awaitable<URL>;
145
+ /**
146
+ * Extra query parameters to append to the authorization URL.
147
+ * Providers forward these to the shared `createAuthorizationURL` helper,
148
+ * which drops any keys present in `RESERVED_AUTHORIZATION_PARAMS`
149
+ * before applying them.
150
+ */
151
+ additionalParams?: Record<string, string> | undefined;
152
+ }) => Awaitable<AuthorizationURLResult>;
39
153
  name: string;
40
154
  validateAuthorizationCode: (data: {
41
155
  code: string;
@@ -69,16 +183,12 @@ export interface OAuthProvider<
69
183
  refreshAccessToken?:
70
184
  | ((refreshToken: string) => Promise<OAuth2Tokens>)
71
185
  | undefined;
72
- revokeToken?: ((token: string) => Promise<void>) | undefined;
73
186
  /**
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
187
+ * Declarative id_token verification config consumed by the shared
188
+ * `verifyProviderIdToken` verifier. Providers set this instead of implementing a boolean
189
+ * verify method, which keeps verification centralized and fail-closed.
78
190
  */
79
- verifyIdToken?:
80
- | ((token: string, nonce?: string) => Promise<boolean>)
81
- | undefined;
191
+ idToken?: OAuthIdTokenConfig | undefined;
82
192
  /**
83
193
  * The expected issuer identifier for this provider (RFC 9207).
84
194
  * When set, the callback handler validates the `iss` query parameter
@@ -94,6 +204,17 @@ export interface OAuthProvider<
94
204
  * Disable sign up for new users.
95
205
  */
96
206
  disableSignUp?: boolean | undefined;
207
+ /**
208
+ * Accept callbacks that arrive without a `state` parameter. When true,
209
+ * the shared OAuth callback handler restarts the flow server-side with
210
+ * fresh `state` and PKCE instead of rejecting the request. Intended for
211
+ * providers that initiate OAuth without RP-side flow kickoff (e.g.
212
+ * Clever). Leave unset for any provider that always initiates from the
213
+ * RP.
214
+ *
215
+ * @default false
216
+ */
217
+ allowIdpInitiated?: boolean | undefined;
97
218
  /**
98
219
  * Options for the provider
99
220
  */
@@ -104,9 +225,10 @@ export type ProviderOptions<Profile extends Record<string, any> = any> = {
104
225
  /**
105
226
  * The client ID of your application.
106
227
  *
107
- * This is usually a string but can be any type depending on the provider.
228
+ * Some providers accept multiple platform client IDs. The first entry is the
229
+ * primary client ID used for token endpoint client authentication.
108
230
  */
109
- clientId?: unknown | undefined;
231
+ clientId?: LiteralString | string[] | undefined;
110
232
  /**
111
233
  * The client secret of your application
112
234
  */
@@ -161,6 +283,10 @@ export type ProviderOptions<Profile extends Record<string, any> = any> = {
161
283
  emailVerified: boolean;
162
284
  [key: string]: any;
163
285
  };
286
+ // TODO: type as `Profile` once provider getUserInfo paths that return a
287
+ // narrower data shape than their declared profile are reconciled; today
288
+ // `any` is load-bearing for those (e.g. facebook) and tightening it ripples
289
+ // across ~10 providers, out of scope for the grant refactor.
164
290
  data: any;
165
291
  } | null>)
166
292
  | undefined;
@@ -225,4 +351,27 @@ export type ProviderOptions<Profile extends Record<string, any> = any> = {
225
351
  * @default false
226
352
  */
227
353
  overrideUserInfoOnSignIn?: boolean | undefined;
354
+ /**
355
+ * Require this provider's email to be verified before a session is created.
356
+ *
357
+ * When the provider reports the email as unverified, the user and account are
358
+ * still created/linked, but no session is issued: the OAuth callback redirects
359
+ * with `?error=email_not_verified` and id-token sign-in returns a `403`
360
+ * `EMAIL_NOT_VERIFIED`. A verification email is (re)sent per the
361
+ * `emailVerification` settings (`sendOnSignUp` / `sendOnSignIn`).
362
+ *
363
+ * The gate checks the local user's verification state, not the provider's
364
+ * claim on each request: a user already verified through another method (or a
365
+ * prior verified sign-in) keeps access even if the provider later reports the
366
+ * email as unverified.
367
+ *
368
+ * This is opt-in per provider and is independent of
369
+ * `emailAndPassword.requireEmailVerification`; enabling that does not gate
370
+ * social sign-in. Only enable it for providers that report a trustworthy
371
+ * `email_verified` signal: several providers always report the email as
372
+ * unverified, which would block every sign-in.
373
+ *
374
+ * @default false
375
+ */
376
+ requireEmailVerification?: boolean | undefined;
228
377
  };
@@ -1,99 +1,78 @@
1
- import { base64 } from "@better-auth/utils/base64";
2
1
  import { betterFetch } from "@better-fetch/fetch";
3
2
  import type { AwaitableFunction } from "../types";
4
- import type { ClientAssertionConfig } from "./client-assertion";
5
- import { resolveAssertionParams } from "./client-assertion";
6
3
  import type { OAuth2Tokens, ProviderOptions } from "./oauth-provider";
4
+ import type {
5
+ TokenEndpointAuth,
6
+ TokenEndpointSecretAuthentication,
7
+ } from "./token-endpoint-auth";
8
+ import { applyTokenEndpointAuth } from "./token-endpoint-auth";
9
+
10
+ interface RefreshAccessTokenRequestInput {
11
+ refreshToken: string;
12
+ options: AwaitableFunction<Partial<ProviderOptions>>;
13
+ authentication?: TokenEndpointSecretAuthentication | undefined;
14
+ tokenEndpointAuth?: TokenEndpointAuth | undefined;
15
+ tokenEndpoint?: string | undefined;
16
+ extraParams?: Record<string, string> | undefined;
17
+ resource?: (string | string[]) | undefined;
18
+ }
19
+
20
+ interface RefreshAccessTokenRequestBaseInput {
21
+ refreshToken: string;
22
+ options: ProviderOptions;
23
+ extraParams?: Record<string, string> | undefined;
24
+ resource?: (string | string[]) | undefined;
25
+ }
26
+
27
+ interface RefreshAccessTokenInput extends RefreshAccessTokenRequestInput {
28
+ options: Partial<ProviderOptions>;
29
+ tokenEndpoint: string;
30
+ }
7
31
 
8
32
  export async function refreshAccessTokenRequest({
9
33
  refreshToken,
10
34
  options,
11
35
  authentication,
12
- clientAssertion,
36
+ tokenEndpointAuth,
13
37
  tokenEndpoint,
14
38
  extraParams,
15
39
  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
- }) {
40
+ }: RefreshAccessTokenRequestInput) {
26
41
  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({
42
+ const request = buildRefreshAccessTokenRequest({
46
43
  refreshToken,
47
44
  options,
48
- authentication,
49
45
  extraParams,
50
46
  resource,
51
47
  });
48
+
49
+ await applyTokenEndpointAuth({
50
+ body: request.body,
51
+ headers: request.headers,
52
+ options,
53
+ tokenEndpoint: tokenEndpoint ?? "",
54
+ grantType: "refresh_token",
55
+ tokenEndpointAuth,
56
+ authentication,
57
+ });
58
+
59
+ return request;
52
60
  }
53
61
 
54
- /**
55
- * @deprecated use async'd refreshAccessTokenRequest instead
56
- */
57
- export function createRefreshAccessTokenRequest({
62
+ function buildRefreshAccessTokenRequest({
58
63
  refreshToken,
59
64
  options,
60
- authentication,
61
65
  extraParams,
62
66
  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
- }) {
67
+ }: RefreshAccessTokenRequestBaseInput) {
70
68
  const body = new URLSearchParams();
71
- const headers: Record<string, any> = {
69
+ const headers: Record<string, string> = {
72
70
  "content-type": "application/x-www-form-urlencoded",
73
71
  accept: "application/json",
74
72
  };
75
73
 
76
74
  body.set("grant_type", "refresh_token");
77
75
  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
76
  if (resource) {
98
77
  if (typeof resource === "string") {
99
78
  body.append("resource", resource);
@@ -120,23 +99,18 @@ export async function refreshAccessToken({
120
99
  options,
121
100
  tokenEndpoint,
122
101
  authentication,
123
- clientAssertion,
102
+ tokenEndpointAuth,
124
103
  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> {
104
+ resource,
105
+ }: RefreshAccessTokenInput): Promise<OAuth2Tokens> {
133
106
  const { body, headers } = await refreshAccessTokenRequest({
134
107
  refreshToken,
135
108
  options,
136
109
  authentication,
137
- clientAssertion,
110
+ tokenEndpointAuth,
138
111
  tokenEndpoint,
139
112
  extraParams,
113
+ resource,
140
114
  });
141
115
 
142
116
  const { data, error } = await betterFetch<{
@@ -145,7 +119,7 @@ export async function refreshAccessToken({
145
119
  expires_in?: number | undefined;
146
120
  refresh_token_expires_in?: number | undefined;
147
121
  token_type?: string | undefined;
148
- scope?: string | undefined;
122
+ scope?: (string | string[]) | undefined;
149
123
  id_token?: string | undefined;
150
124
  }>(tokenEndpoint, {
151
125
  method: "POST",
@@ -159,7 +133,7 @@ export async function refreshAccessToken({
159
133
  accessToken: data.access_token,
160
134
  refreshToken: data.refresh_token,
161
135
  tokenType: data.token_type,
162
- scopes: data.scope?.split(" "),
136
+ scopes: Array.isArray(data.scope) ? data.scope : data.scope?.split(" "),
163
137
  idToken: data.id_token,
164
138
  };
165
139
 
@@ -0,0 +1,118 @@
1
+ import type { ProviderOptions } from "./oauth-provider";
2
+
3
+ /**
4
+ * Parse a provider's `scope` token-response field into a string array.
5
+ *
6
+ * RFC 6749 §3.3 defines `scope` as a space-delimited string, but providers
7
+ * vary: some (e.g. Twitch) return an already-split array. Accept both, plus the
8
+ * omitted/empty case, without ever calling `.split` on a non-string. Returns
9
+ * `[]` when no scope is present.
10
+ *
11
+ * @see https://github.com/better-auth/better-auth/issues/9076
12
+ */
13
+ export function parseScopeField(scope: unknown): string[] {
14
+ if (Array.isArray(scope))
15
+ return scope.filter((s): s is string => typeof s === "string" && s !== "");
16
+ if (typeof scope === "string") return scope.split(" ").filter(Boolean);
17
+ return [];
18
+ }
19
+
20
+ /**
21
+ * Normalize a scope set into a single deduped, sorted array.
22
+ *
23
+ * Scope order is insignificant per RFC 6749 §3.3, so normalize for idempotent
24
+ * writes and trivial comparisons: trim each token, drop empties, dedupe, and
25
+ * sort ascending. Returns `[]` when the union is empty.
26
+ *
27
+ * @see https://www.rfc-editor.org/rfc/rfc6749#section-3.3
28
+ */
29
+ export function normalizeScopes(
30
+ stored: string[] | null | undefined,
31
+ incoming?: string[] | undefined,
32
+ ): string[] {
33
+ const normalized = new Set<string>();
34
+ for (const scope of [...(stored ?? []), ...(incoming ?? [])]) {
35
+ const trimmed = scope.trim();
36
+ if (trimmed) normalized.add(trimmed);
37
+ }
38
+ return [...normalized].sort();
39
+ }
40
+
41
+ /**
42
+ * Union the stored granted-scope set with the scopes observed on an
43
+ * authorization or token exchange.
44
+ *
45
+ * The provider's echoed `scope` is authoritative when present. RFC 6749 §3.3
46
+ * and §5.1 say an omitted or empty echo means the grant equals what was
47
+ * requested, so fall back to `requested` in that case. The result unions onto
48
+ * the stored grant (never narrows on a normal write) and is normalized per
49
+ * {@link normalizeScopes}.
50
+ *
51
+ * @see https://www.rfc-editor.org/rfc/rfc6749#section-3.3
52
+ * @see https://www.rfc-editor.org/rfc/rfc6749#section-5.1
53
+ */
54
+ export function unionGrantedScopes(
55
+ stored: string[] | null | undefined,
56
+ echoed: string[] | undefined,
57
+ requested: string[] | undefined,
58
+ ): string[] {
59
+ const granted = echoed?.length ? echoed : requested;
60
+ return normalizeScopes(stored, granted);
61
+ }
62
+
63
+ /**
64
+ * Coerce a stored granted-scope value into a usable array.
65
+ *
66
+ * `account.grantedScopes` is nullable (legacy rows and non-OAuth accounts read
67
+ * as unset), and on dialects that store the array as a JSON string a malformed
68
+ * operator backfill could deserialize to a non-array. Both collapse to `[]`
69
+ * here so every reader works against a real `string[]` without re-deriving the
70
+ * guard.
71
+ */
72
+ export function readGrantedScopes(
73
+ stored: string[] | null | undefined,
74
+ ): string[] {
75
+ return Array.isArray(stored) ? stored : [];
76
+ }
77
+
78
+ /**
79
+ * Test whether a normalized granted-scope set contains a specific scope.
80
+ *
81
+ * Matching is exact and case-sensitive per RFC 6749 §3.3. The argument is the
82
+ * normalized `account.grantedScopes` array; a raw provider `scope` string must
83
+ * be run through {@link parseScopeField} first.
84
+ *
85
+ * @see https://www.rfc-editor.org/rfc/rfc6749#section-3.3
86
+ */
87
+ export function includesGrantedScope(
88
+ granted: string[] | null | undefined,
89
+ scope: string,
90
+ ): boolean {
91
+ return granted?.includes(scope) ?? false;
92
+ }
93
+
94
+ /**
95
+ * Compose the effective scope set to encode in a single authorization URL.
96
+ *
97
+ * Precedence: the provider's built-in defaults (unless `disableDefaultScope`),
98
+ * then the integrator's configured `options.scope`, then the per-request
99
+ * `scopes`. The result is the value persisted into OAuth state as the RFC 6749
100
+ * §5.1 fallback, so it is preserved verbatim (not normalized) to match what is
101
+ * sent to the provider.
102
+ *
103
+ * `defaultScopes` is a parameter rather than a provider-contract field so the
104
+ * runtime-synthesized generic OAuth provider, which has no static default set,
105
+ * can pass its configured scopes here.
106
+ *
107
+ * @see https://www.rfc-editor.org/rfc/rfc6749#section-5.1
108
+ */
109
+ export function resolveRequestedScopes(
110
+ options: Pick<ProviderOptions, "scope" | "disableDefaultScope"> | undefined,
111
+ defaultScopes: string[],
112
+ perRequestScopes: string[] | undefined,
113
+ ): string[] {
114
+ const scopes = options?.disableDefaultScope ? [] : [...defaultScopes];
115
+ if (options?.scope) scopes.push(...options.scope);
116
+ if (perRequestScopes) scopes.push(...perRequestScopes);
117
+ return scopes;
118
+ }