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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/dist/context/global.mjs +1 -1
  2. package/dist/db/adapter/factory.mjs +63 -1
  3. package/dist/db/adapter/index.d.mts +35 -1
  4. package/dist/db/adapter/types.d.mts +1 -1
  5. package/dist/db/type.d.mts +12 -0
  6. package/dist/error/codes.d.mts +1 -0
  7. package/dist/error/codes.mjs +1 -0
  8. package/dist/instrumentation/tracer.mjs +1 -1
  9. package/dist/oauth2/authorization-params.d.mts +12 -0
  10. package/dist/oauth2/authorization-params.mjs +12 -0
  11. package/dist/oauth2/basic-credentials.d.mts +30 -0
  12. package/dist/oauth2/basic-credentials.mjs +64 -0
  13. package/dist/oauth2/client-assertion.d.mts +38 -22
  14. package/dist/oauth2/client-assertion.mjs +63 -28
  15. package/dist/oauth2/client-credentials-token.d.mts +19 -40
  16. package/dist/oauth2/client-credentials-token.mjs +18 -29
  17. package/dist/oauth2/create-authorization-url.d.mts +9 -1
  18. package/dist/oauth2/create-authorization-url.mjs +23 -5
  19. package/dist/oauth2/index.d.mts +10 -7
  20. package/dist/oauth2/index.mjs +9 -7
  21. package/dist/oauth2/oauth-provider.d.mts +21 -2
  22. package/dist/oauth2/refresh-access-token.d.mts +20 -40
  23. package/dist/oauth2/refresh-access-token.mjs +19 -32
  24. package/dist/oauth2/token-endpoint-auth.d.mts +17 -0
  25. package/dist/oauth2/token-endpoint-auth.mjs +89 -0
  26. package/dist/oauth2/utils.d.mts +9 -1
  27. package/dist/oauth2/utils.mjs +12 -1
  28. package/dist/oauth2/validate-authorization-code.d.mts +17 -52
  29. package/dist/oauth2/validate-authorization-code.mjs +17 -30
  30. package/dist/oauth2/verify.mjs +15 -5
  31. package/dist/social-providers/apple.d.mts +4 -18
  32. package/dist/social-providers/apple.mjs +14 -3
  33. package/dist/social-providers/atlassian.d.mts +3 -1
  34. package/dist/social-providers/atlassian.mjs +5 -2
  35. package/dist/social-providers/cognito.d.mts +16 -1
  36. package/dist/social-providers/cognito.mjs +6 -2
  37. package/dist/social-providers/discord.d.mts +4 -2
  38. package/dist/social-providers/discord.mjs +16 -3
  39. package/dist/social-providers/dropbox.d.mts +3 -1
  40. package/dist/social-providers/dropbox.mjs +5 -4
  41. package/dist/social-providers/facebook.d.mts +3 -1
  42. package/dist/social-providers/facebook.mjs +5 -2
  43. package/dist/social-providers/figma.d.mts +3 -1
  44. package/dist/social-providers/figma.mjs +3 -2
  45. package/dist/social-providers/github.d.mts +3 -1
  46. package/dist/social-providers/github.mjs +5 -4
  47. package/dist/social-providers/gitlab.d.mts +3 -1
  48. package/dist/social-providers/gitlab.mjs +3 -2
  49. package/dist/social-providers/google.d.mts +3 -1
  50. package/dist/social-providers/google.mjs +5 -2
  51. package/dist/social-providers/huggingface.d.mts +3 -1
  52. package/dist/social-providers/huggingface.mjs +3 -2
  53. package/dist/social-providers/index.d.mts +104 -36
  54. package/dist/social-providers/kakao.d.mts +3 -1
  55. package/dist/social-providers/kakao.mjs +3 -2
  56. package/dist/social-providers/kick.d.mts +3 -1
  57. package/dist/social-providers/kick.mjs +3 -2
  58. package/dist/social-providers/line.d.mts +3 -1
  59. package/dist/social-providers/line.mjs +3 -2
  60. package/dist/social-providers/linear.d.mts +3 -1
  61. package/dist/social-providers/linear.mjs +3 -2
  62. package/dist/social-providers/linkedin.d.mts +3 -1
  63. package/dist/social-providers/linkedin.mjs +3 -2
  64. package/dist/social-providers/microsoft-entra-id.d.mts +2 -1
  65. package/dist/social-providers/microsoft-entra-id.mjs +3 -2
  66. package/dist/social-providers/naver.d.mts +3 -1
  67. package/dist/social-providers/naver.mjs +3 -2
  68. package/dist/social-providers/notion.d.mts +3 -1
  69. package/dist/social-providers/notion.mjs +5 -2
  70. package/dist/social-providers/paybin.d.mts +3 -1
  71. package/dist/social-providers/paybin.mjs +3 -2
  72. package/dist/social-providers/paypal.d.mts +3 -1
  73. package/dist/social-providers/paypal.mjs +4 -3
  74. package/dist/social-providers/polar.d.mts +3 -1
  75. package/dist/social-providers/polar.mjs +3 -2
  76. package/dist/social-providers/railway.d.mts +3 -1
  77. package/dist/social-providers/railway.mjs +3 -2
  78. package/dist/social-providers/reddit.d.mts +3 -1
  79. package/dist/social-providers/reddit.mjs +3 -2
  80. package/dist/social-providers/roblox.d.mts +4 -2
  81. package/dist/social-providers/roblox.mjs +12 -2
  82. package/dist/social-providers/salesforce.d.mts +3 -1
  83. package/dist/social-providers/salesforce.mjs +3 -2
  84. package/dist/social-providers/slack.d.mts +4 -2
  85. package/dist/social-providers/slack.mjs +11 -8
  86. package/dist/social-providers/spotify.d.mts +3 -1
  87. package/dist/social-providers/spotify.mjs +3 -2
  88. package/dist/social-providers/tiktok.d.mts +3 -1
  89. package/dist/social-providers/tiktok.mjs +14 -2
  90. package/dist/social-providers/twitch.d.mts +3 -1
  91. package/dist/social-providers/twitch.mjs +3 -2
  92. package/dist/social-providers/twitter.d.mts +5 -2
  93. package/dist/social-providers/twitter.mjs +2 -1
  94. package/dist/social-providers/vercel.d.mts +3 -1
  95. package/dist/social-providers/vercel.mjs +3 -2
  96. package/dist/social-providers/vk.d.mts +3 -1
  97. package/dist/social-providers/vk.mjs +3 -2
  98. package/dist/social-providers/wechat.d.mts +3 -1
  99. package/dist/social-providers/wechat.mjs +7 -1
  100. package/dist/social-providers/zoom.d.mts +4 -2
  101. package/dist/social-providers/zoom.mjs +10 -17
  102. package/dist/types/context.d.mts +23 -3
  103. package/dist/types/init-options.d.mts +29 -5
  104. package/dist/utils/ip.d.mts +5 -4
  105. package/dist/utils/ip.mjs +3 -3
  106. package/dist/utils/redirect-uri.d.mts +20 -0
  107. package/dist/utils/redirect-uri.mjs +48 -0
  108. package/dist/utils/string.d.mts +5 -1
  109. package/dist/utils/string.mjs +20 -1
  110. package/dist/utils/url.d.mts +18 -1
  111. package/dist/utils/url.mjs +30 -1
  112. package/package.json +9 -8
  113. package/src/db/adapter/factory.ts +118 -0
  114. package/src/db/adapter/index.ts +32 -0
  115. package/src/db/adapter/types.ts +1 -0
  116. package/src/db/type.ts +12 -0
  117. package/src/error/codes.ts +1 -0
  118. package/src/oauth2/authorization-params.ts +28 -0
  119. package/src/oauth2/basic-credentials.ts +87 -0
  120. package/src/oauth2/client-assertion.ts +131 -58
  121. package/src/oauth2/client-credentials-token.ts +48 -72
  122. package/src/oauth2/create-authorization-url.ts +28 -6
  123. package/src/oauth2/index.ts +25 -9
  124. package/src/oauth2/oauth-provider.ts +21 -2
  125. package/src/oauth2/refresh-access-token.ts +50 -76
  126. package/src/oauth2/token-endpoint-auth.ts +221 -0
  127. package/src/oauth2/utils.ts +19 -0
  128. package/src/oauth2/validate-authorization-code.ts +55 -85
  129. package/src/oauth2/verify.ts +20 -4
  130. package/src/social-providers/apple.ts +26 -2
  131. package/src/social-providers/atlassian.ts +8 -1
  132. package/src/social-providers/cognito.ts +26 -1
  133. package/src/social-providers/discord.ts +21 -17
  134. package/src/social-providers/dropbox.ts +7 -5
  135. package/src/social-providers/facebook.ts +11 -6
  136. package/src/social-providers/figma.ts +8 -1
  137. package/src/social-providers/github.ts +4 -2
  138. package/src/social-providers/gitlab.ts +2 -0
  139. package/src/social-providers/google.ts +2 -0
  140. package/src/social-providers/huggingface.ts +8 -1
  141. package/src/social-providers/kakao.ts +2 -1
  142. package/src/social-providers/kick.ts +8 -1
  143. package/src/social-providers/line.ts +2 -0
  144. package/src/social-providers/linear.ts +8 -1
  145. package/src/social-providers/linkedin.ts +2 -0
  146. package/src/social-providers/microsoft-entra-id.ts +1 -0
  147. package/src/social-providers/naver.ts +2 -1
  148. package/src/social-providers/notion.ts +8 -1
  149. package/src/social-providers/paybin.ts +2 -0
  150. package/src/social-providers/paypal.ts +7 -1
  151. package/src/social-providers/polar.ts +8 -1
  152. package/src/social-providers/railway.ts +8 -1
  153. package/src/social-providers/reddit.ts +2 -1
  154. package/src/social-providers/roblox.ts +16 -11
  155. package/src/social-providers/salesforce.ts +8 -1
  156. package/src/social-providers/slack.ts +15 -9
  157. package/src/social-providers/spotify.ts +8 -1
  158. package/src/social-providers/tiktok.ts +22 -9
  159. package/src/social-providers/twitch.ts +2 -1
  160. package/src/social-providers/twitter.ts +1 -0
  161. package/src/social-providers/vercel.ts +8 -1
  162. package/src/social-providers/vk.ts +8 -1
  163. package/src/social-providers/wechat.ts +9 -1
  164. package/src/social-providers/zoom.ts +15 -19
  165. package/src/types/context.ts +25 -4
  166. package/src/types/init-options.ts +29 -5
  167. package/src/utils/ip.ts +12 -13
  168. package/src/utils/redirect-uri.ts +54 -0
  169. package/src/utils/string.ts +37 -0
  170. package/src/utils/url.ts +28 -0
@@ -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<{
@@ -0,0 +1,221 @@
1
+ import { encodeBasicCredentials } from "./basic-credentials";
2
+ import type {
3
+ ClientAssertionGetter,
4
+ ClientAssertionGrantType,
5
+ } from "./client-assertion";
6
+ import { resolveClientAssertionParams } from "./client-assertion";
7
+ import { getPrimaryClientId } from "./utils";
8
+
9
+ export type TokenEndpointAuth =
10
+ | {
11
+ method: "none";
12
+ }
13
+ | {
14
+ method: "client_secret_basic";
15
+ }
16
+ | {
17
+ method: "client_secret_post";
18
+ }
19
+ | {
20
+ method: "private_key_jwt";
21
+ getClientAssertion: ClientAssertionGetter;
22
+ };
23
+
24
+ export type TokenEndpointAuthMethod = TokenEndpointAuth["method"];
25
+
26
+ export type TokenEndpointSecretAuthentication = "basic" | "post";
27
+
28
+ export interface TokenEndpointClientOptions {
29
+ clientId?: string | string[] | undefined;
30
+ clientSecret?: string | undefined;
31
+ }
32
+
33
+ export interface ApplyTokenEndpointAuthInput {
34
+ body: URLSearchParams;
35
+ headers: Record<string, string>;
36
+ options: TokenEndpointClientOptions;
37
+ tokenEndpoint: string;
38
+ grantType: ClientAssertionGrantType;
39
+ tokenEndpointAuth?: TokenEndpointAuth | undefined;
40
+ authentication?: TokenEndpointSecretAuthentication | undefined;
41
+ }
42
+
43
+ function getDefaultTokenEndpointAuth(
44
+ options: TokenEndpointClientOptions,
45
+ authentication?: TokenEndpointSecretAuthentication,
46
+ ): TokenEndpointAuth {
47
+ if (authentication === "basic") {
48
+ return { method: "client_secret_basic" };
49
+ }
50
+ if (options.clientSecret) {
51
+ return { method: "client_secret_post" };
52
+ }
53
+ return { method: "none" };
54
+ }
55
+
56
+ function assertNoClientSecret(
57
+ method: "none" | "private_key_jwt",
58
+ options: TokenEndpointClientOptions,
59
+ body: URLSearchParams,
60
+ ) {
61
+ if (options.clientSecret || body.has("client_secret")) {
62
+ throw new Error(
63
+ `${method} token endpoint authentication cannot be combined with clientSecret`,
64
+ );
65
+ }
66
+ }
67
+
68
+ function setClientId(body: URLSearchParams, clientId: string | undefined) {
69
+ if (clientId) {
70
+ body.set("client_id", clientId);
71
+ }
72
+ }
73
+
74
+ function assertClientSecretConfigured(
75
+ method: "client_secret_basic" | "client_secret_post",
76
+ options: TokenEndpointClientOptions,
77
+ ): asserts options is TokenEndpointClientOptions & { clientSecret: string } {
78
+ if (!options.clientSecret) {
79
+ throw new Error(
80
+ `${method} token endpoint authentication requires clientSecret`,
81
+ );
82
+ }
83
+ }
84
+
85
+ function assertClientIdConfigured(
86
+ method: TokenEndpointAuthMethod,
87
+ clientId: string | undefined,
88
+ ): asserts clientId is string {
89
+ if (!clientId) {
90
+ throw new Error(
91
+ `${method} token endpoint authentication requires clientId`,
92
+ );
93
+ }
94
+ }
95
+
96
+ function setClientSecretPostAuth({
97
+ body,
98
+ options,
99
+ clientId,
100
+ requireClientSecret,
101
+ }: {
102
+ body: URLSearchParams;
103
+ options: TokenEndpointClientOptions;
104
+ clientId: string | undefined;
105
+ requireClientSecret?: boolean | undefined;
106
+ }) {
107
+ if (requireClientSecret) {
108
+ assertClientSecretConfigured("client_secret_post", options);
109
+ }
110
+ if (options.clientSecret) {
111
+ assertClientIdConfigured("client_secret_post", clientId);
112
+ setClientId(body, clientId);
113
+ body.set("client_secret", options.clientSecret);
114
+ }
115
+ }
116
+
117
+ function setClientSecretBasicAuth({
118
+ headers,
119
+ options,
120
+ clientId,
121
+ body,
122
+ }: {
123
+ headers: Record<string, string>;
124
+ options: TokenEndpointClientOptions;
125
+ clientId: string | undefined;
126
+ body: URLSearchParams;
127
+ }) {
128
+ if (body.has("client_secret")) {
129
+ throw new Error(
130
+ "client_secret_basic token endpoint authentication cannot be combined with client_secret body parameters",
131
+ );
132
+ }
133
+ assertClientSecretConfigured("client_secret_basic", options);
134
+ assertClientIdConfigured("client_secret_basic", clientId);
135
+ headers.authorization = encodeBasicCredentials(
136
+ clientId,
137
+ options.clientSecret,
138
+ );
139
+ }
140
+
141
+ function assertCompleteManualClientAssertion(body: URLSearchParams) {
142
+ if (body.has("client_assertion") !== body.has("client_assertion_type")) {
143
+ throw new Error(
144
+ "client_assertion and client_assertion_type must both be provided",
145
+ );
146
+ }
147
+ }
148
+
149
+ export async function applyTokenEndpointAuth({
150
+ body,
151
+ headers,
152
+ options,
153
+ tokenEndpoint,
154
+ grantType,
155
+ tokenEndpointAuth,
156
+ authentication,
157
+ }: ApplyTokenEndpointAuthInput) {
158
+ assertCompleteManualClientAssertion(body);
159
+
160
+ const clientId = getPrimaryClientId(options.clientId);
161
+ if (body.has("client_assertion")) {
162
+ if (tokenEndpointAuth) {
163
+ throw new Error(
164
+ "client_assertion body parameters cannot be combined with tokenEndpointAuth",
165
+ );
166
+ }
167
+ assertNoClientSecret("private_key_jwt", options, body);
168
+ setClientId(body, clientId);
169
+ return;
170
+ }
171
+
172
+ const auth =
173
+ tokenEndpointAuth ?? getDefaultTokenEndpointAuth(options, authentication);
174
+
175
+ if (auth.method === "private_key_jwt") {
176
+ assertNoClientSecret(auth.method, options, body);
177
+ assertClientIdConfigured(auth.method, clientId);
178
+ if (!tokenEndpoint) {
179
+ throw new Error(
180
+ "private_key_jwt token endpoint authentication requires tokenEndpoint",
181
+ );
182
+ }
183
+ const assertionParams = await resolveClientAssertionParams({
184
+ getClientAssertion: auth.getClientAssertion,
185
+ context: {
186
+ clientId,
187
+ tokenEndpoint,
188
+ grantType,
189
+ },
190
+ });
191
+ setClientId(body, clientId);
192
+ for (const [key, value] of Object.entries(assertionParams)) {
193
+ body.set(key, value);
194
+ }
195
+ return;
196
+ }
197
+
198
+ if (auth.method === "none") {
199
+ assertNoClientSecret(auth.method, options, body);
200
+ if (grantType === "client_credentials") {
201
+ throw new Error(
202
+ "none token endpoint authentication cannot be used with client_credentials grant",
203
+ );
204
+ }
205
+ assertClientIdConfigured(auth.method, clientId);
206
+ setClientId(body, clientId);
207
+ return;
208
+ }
209
+
210
+ if (auth.method === "client_secret_basic") {
211
+ setClientSecretBasicAuth({ headers, options, clientId, body });
212
+ return;
213
+ }
214
+
215
+ setClientSecretPostAuth({
216
+ body,
217
+ options,
218
+ clientId,
219
+ requireClientSecret: tokenEndpointAuth?.method === "client_secret_post",
220
+ });
221
+ }
@@ -28,6 +28,25 @@ export function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens {
28
28
  };
29
29
  }
30
30
 
31
+ /**
32
+ * Fill in `accessTokenExpiresAt` from the provider's configured
33
+ * `accessTokenExpiresIn` when the token response omitted `expires_in`. Without a
34
+ * known expiry, `getAccessToken` cannot tell the token is expired and never
35
+ * refreshes it. No-op when the provider already supplied an expiry or no
36
+ * fallback is configured.
37
+ */
38
+ export function applyDefaultAccessTokenExpiry(
39
+ tokens: OAuth2Tokens,
40
+ accessTokenExpiresIn: number | undefined,
41
+ ): OAuth2Tokens {
42
+ if (!tokens.accessTokenExpiresAt && accessTokenExpiresIn) {
43
+ tokens.accessTokenExpiresAt = new Date(
44
+ Date.now() + accessTokenExpiresIn * 1000,
45
+ );
46
+ }
47
+ return tokens;
48
+ }
49
+
31
50
  /**
32
51
  * Return the provider's primary Client ID: the single string, or the entry at
33
52
  * array index 0 for the cross-platform form used by ID token audience
@@ -1,11 +1,42 @@
1
- import { base64 } from "@better-auth/utils/base64";
2
1
  import { betterFetch } from "@better-fetch/fetch";
3
2
  import { createRemoteJWKSet, jwtVerify } from "jose";
4
3
  import type { AwaitableFunction } from "../types";
5
- import type { ClientAssertionConfig } from "./client-assertion";
6
- import { resolveAssertionParams } from "./client-assertion";
7
4
  import type { ProviderOptions } from "./index";
8
5
  import { getOAuth2Tokens } from "./index";
6
+ import type {
7
+ TokenEndpointAuth,
8
+ TokenEndpointSecretAuthentication,
9
+ } from "./token-endpoint-auth";
10
+ import { applyTokenEndpointAuth } from "./token-endpoint-auth";
11
+
12
+ interface AuthorizationCodeRequestInput {
13
+ code: string;
14
+ redirectURI: string;
15
+ options: AwaitableFunction<Partial<ProviderOptions>>;
16
+ codeVerifier?: string | undefined;
17
+ deviceId?: string | undefined;
18
+ authentication?: TokenEndpointSecretAuthentication | undefined;
19
+ tokenEndpointAuth?: TokenEndpointAuth | undefined;
20
+ tokenEndpoint?: string | undefined;
21
+ headers?: Record<string, string> | undefined;
22
+ additionalParams?: Record<string, string> | undefined;
23
+ resource?: (string | string[]) | undefined;
24
+ }
25
+
26
+ interface AuthorizationCodeRequestBaseInput {
27
+ code: string;
28
+ redirectURI: string;
29
+ options: Partial<ProviderOptions>;
30
+ codeVerifier?: string | undefined;
31
+ deviceId?: string | undefined;
32
+ headers?: Record<string, string> | undefined;
33
+ additionalParams?: Record<string, string> | undefined;
34
+ resource?: (string | string[]) | undefined;
35
+ }
36
+
37
+ interface ValidateAuthorizationCodeInput extends AuthorizationCodeRequestInput {
38
+ tokenEndpoint: string;
39
+ }
9
40
 
10
41
  export async function authorizationCodeRequest({
11
42
  code,
@@ -13,84 +44,50 @@ export async function authorizationCodeRequest({
13
44
  redirectURI,
14
45
  options,
15
46
  authentication,
16
- clientAssertion,
47
+ tokenEndpointAuth,
17
48
  tokenEndpoint,
18
49
  deviceId,
19
50
  headers,
20
51
  additionalParams = {},
21
52
  resource,
22
- }: {
23
- code: string;
24
- redirectURI: string;
25
- options: AwaitableFunction<Partial<ProviderOptions>>;
26
- codeVerifier?: string | undefined;
27
- deviceId?: string | undefined;
28
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
29
- clientAssertion?: ClientAssertionConfig | undefined;
30
- /** Token endpoint URL. Used as the JWT `aud` claim when signing assertions. */
31
- tokenEndpoint?: string | undefined;
32
- headers?: Record<string, string> | undefined;
33
- additionalParams?: Record<string, string> | undefined;
34
- resource?: (string | string[]) | undefined;
35
- }) {
53
+ }: AuthorizationCodeRequestInput) {
36
54
  options = typeof options === "function" ? await options() : options;
37
-
38
- if (authentication === "private_key_jwt") {
39
- if (!clientAssertion) {
40
- throw new Error(
41
- "private_key_jwt authentication requires a clientAssertion configuration",
42
- );
43
- }
44
- const primaryClientId = Array.isArray(options.clientId)
45
- ? options.clientId[0]
46
- : options.clientId;
47
- const assertionParams = await resolveAssertionParams({
48
- clientAssertion,
49
- clientId: primaryClientId,
50
- tokenEndpoint,
51
- });
52
- additionalParams = { ...additionalParams, ...assertionParams };
53
- }
54
-
55
- return createAuthorizationCodeRequest({
55
+ const request = buildAuthorizationCodeRequest({
56
56
  code,
57
57
  codeVerifier,
58
58
  redirectURI,
59
59
  options,
60
- authentication,
61
60
  deviceId,
62
61
  headers,
63
62
  additionalParams,
64
63
  resource,
65
64
  });
65
+
66
+ await applyTokenEndpointAuth({
67
+ body: request.body,
68
+ headers: request.headers,
69
+ options,
70
+ tokenEndpoint: tokenEndpoint ?? "",
71
+ grantType: "authorization_code",
72
+ tokenEndpointAuth,
73
+ authentication,
74
+ });
75
+
76
+ return request;
66
77
  }
67
78
 
68
- /**
69
- * @deprecated use async'd authorizationCodeRequest instead
70
- */
71
- export function createAuthorizationCodeRequest({
79
+ function buildAuthorizationCodeRequest({
72
80
  code,
73
81
  codeVerifier,
74
82
  redirectURI,
75
83
  options,
76
- authentication,
77
84
  deviceId,
78
85
  headers,
79
86
  additionalParams = {},
80
87
  resource,
81
- }: {
82
- code: string;
83
- redirectURI: string;
84
- options: Partial<ProviderOptions>;
85
- codeVerifier?: string | undefined;
86
- deviceId?: string | undefined;
87
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
88
- headers?: Record<string, string> | undefined;
89
- additionalParams?: Record<string, string> | undefined;
90
- resource?: (string | string[]) | undefined;
91
- }) {
88
+ }: AuthorizationCodeRequestBaseInput) {
92
89
  const body = new URLSearchParams();
93
- const requestHeaders: Record<string, any> = {
90
+ const requestHeaders: Record<string, string> = {
94
91
  "content-type": "application/x-www-form-urlencoded",
95
92
  accept: "application/json",
96
93
  ...headers,
@@ -111,21 +108,6 @@ export function createAuthorizationCodeRequest({
111
108
  }
112
109
  }
113
110
  }
114
- const primaryClientId = Array.isArray(options.clientId)
115
- ? options.clientId[0]
116
- : options.clientId;
117
- if (authentication === "basic") {
118
- const encodedCredentials = base64.encode(
119
- `${primaryClientId}:${options.clientSecret ?? ""}`,
120
- );
121
- requestHeaders["authorization"] = `Basic ${encodedCredentials}`;
122
- } else {
123
- body.set("client_id", primaryClientId);
124
- if (authentication !== "private_key_jwt" && options.clientSecret) {
125
- body.set("client_secret", options.clientSecret);
126
- }
127
- }
128
-
129
111
  for (const [key, value] of Object.entries(additionalParams)) {
130
112
  if (!body.has(key)) body.append(key, value);
131
113
  }
@@ -143,31 +125,19 @@ export async function validateAuthorizationCode({
143
125
  options,
144
126
  tokenEndpoint,
145
127
  authentication,
146
- clientAssertion,
128
+ tokenEndpointAuth,
147
129
  deviceId,
148
130
  headers,
149
131
  additionalParams = {},
150
132
  resource,
151
- }: {
152
- code: string;
153
- redirectURI: string;
154
- options: AwaitableFunction<Partial<ProviderOptions>>;
155
- codeVerifier?: string | undefined;
156
- deviceId?: string | undefined;
157
- tokenEndpoint: string;
158
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
159
- clientAssertion?: ClientAssertionConfig | undefined;
160
- headers?: Record<string, string> | undefined;
161
- additionalParams?: Record<string, string> | undefined;
162
- resource?: (string | string[]) | undefined;
163
- }) {
133
+ }: ValidateAuthorizationCodeInput) {
164
134
  const { body, headers: requestHeaders } = await authorizationCodeRequest({
165
135
  code,
166
136
  codeVerifier,
167
137
  redirectURI,
168
138
  options,
169
139
  authentication,
170
- clientAssertion,
140
+ tokenEndpointAuth,
171
141
  tokenEndpoint,
172
142
  deviceId,
173
143
  headers,
@@ -9,11 +9,22 @@ import type {
9
9
  import {
10
10
  createLocalJWKSet,
11
11
  decodeProtectedHeader,
12
+ errors as joseErrors,
12
13
  jwtVerify,
13
14
  UnsecuredJWT,
14
15
  } from "jose";
15
16
  import { logger } from "../env";
16
17
 
18
+ const joseInfrastructureErrorCodes = new Set([
19
+ joseErrors.JWKSTimeout.code,
20
+ joseErrors.JWKSInvalid.code,
21
+ joseErrors.JWKSMultipleMatchingKeys.code,
22
+ ]);
23
+
24
+ function isJoseInfrastructureError(error: joseErrors.JOSEError) {
25
+ return joseInfrastructureErrorCodes.has(error.code);
26
+ }
27
+
17
28
  /** Last fetched jwks used locally in getJwks @internal */
18
29
  let jwks: JSONWebKeySet | undefined;
19
30
 
@@ -82,7 +93,9 @@ export async function getJwks(
82
93
  throw new Error(error as unknown as string);
83
94
  }
84
95
 
85
- if (!jwtHeaders.kid) throw new Error("Missing jwt kid");
96
+ if (!jwtHeaders.kid) {
97
+ throw new APIError("UNAUTHORIZED", { message: "invalid access token" });
98
+ }
86
99
 
87
100
  // Fetch jwks if not set or has a different kid than the one stored
88
101
  if (!jwks || !jwks.keys.find((jwk) => jwk.kid === jwtHeaders.kid)) {
@@ -137,13 +150,16 @@ export async function verifyAccessToken(
137
150
  if (error instanceof Error) {
138
151
  if (error.name === "TypeError" || error.name === "JWSInvalid") {
139
152
  // likely an opaque token (continue)
140
- } else if (error.name === "JWTExpired") {
153
+ } else if (error instanceof joseErrors.JWTExpired) {
141
154
  throw new APIError("UNAUTHORIZED", {
142
155
  message: "token expired",
143
156
  });
144
- } else if (error.name === "JWTInvalid") {
157
+ } else if (error instanceof joseErrors.JOSEError) {
158
+ if (isJoseInfrastructureError(error)) {
159
+ throw error;
160
+ }
145
161
  throw new APIError("UNAUTHORIZED", {
146
- message: "token invalid",
162
+ message: "invalid access token",
147
163
  });
148
164
  } else {
149
165
  throw error;