@better-auth/core 1.7.0-beta.2 → 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 (172) hide show
  1. package/dist/context/global.mjs +1 -1
  2. package/dist/db/adapter/factory.mjs +64 -3
  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 +5 -12
  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 +5 -3
  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 +5 -3
  42. package/dist/social-providers/facebook.mjs +6 -3
  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 +4 -2
  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 +5 -3
  63. package/dist/social-providers/linkedin.mjs +4 -3
  64. package/dist/social-providers/microsoft-entra-id.d.mts +3 -2
  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 +30 -4
  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 +121 -3
  114. package/src/db/adapter/index.ts +32 -0
  115. package/src/db/adapter/types.ts +1 -0
  116. package/src/db/get-tables.ts +2 -0
  117. package/src/db/schema/user.ts +3 -0
  118. package/src/db/type.ts +12 -0
  119. package/src/error/codes.ts +1 -0
  120. package/src/oauth2/authorization-params.ts +28 -0
  121. package/src/oauth2/basic-credentials.ts +87 -0
  122. package/src/oauth2/client-assertion.ts +131 -58
  123. package/src/oauth2/client-credentials-token.ts +48 -72
  124. package/src/oauth2/create-authorization-url.ts +28 -6
  125. package/src/oauth2/index.ts +25 -9
  126. package/src/oauth2/oauth-provider.ts +21 -2
  127. package/src/oauth2/refresh-access-token.ts +50 -76
  128. package/src/oauth2/token-endpoint-auth.ts +221 -0
  129. package/src/oauth2/utils.ts +19 -0
  130. package/src/oauth2/validate-authorization-code.ts +55 -85
  131. package/src/oauth2/verify.ts +20 -4
  132. package/src/social-providers/apple.ts +27 -3
  133. package/src/social-providers/atlassian.ts +8 -1
  134. package/src/social-providers/cognito.ts +26 -1
  135. package/src/social-providers/discord.ts +22 -18
  136. package/src/social-providers/dropbox.ts +7 -5
  137. package/src/social-providers/facebook.ts +14 -9
  138. package/src/social-providers/figma.ts +8 -1
  139. package/src/social-providers/github.ts +5 -3
  140. package/src/social-providers/gitlab.ts +2 -0
  141. package/src/social-providers/google.ts +2 -0
  142. package/src/social-providers/huggingface.ts +8 -1
  143. package/src/social-providers/kakao.ts +2 -1
  144. package/src/social-providers/kick.ts +8 -1
  145. package/src/social-providers/line.ts +2 -0
  146. package/src/social-providers/linear.ts +8 -1
  147. package/src/social-providers/linkedin.ts +5 -3
  148. package/src/social-providers/microsoft-entra-id.ts +2 -1
  149. package/src/social-providers/naver.ts +2 -1
  150. package/src/social-providers/notion.ts +8 -1
  151. package/src/social-providers/paybin.ts +2 -0
  152. package/src/social-providers/paypal.ts +7 -1
  153. package/src/social-providers/polar.ts +8 -1
  154. package/src/social-providers/railway.ts +8 -1
  155. package/src/social-providers/reddit.ts +2 -1
  156. package/src/social-providers/roblox.ts +16 -11
  157. package/src/social-providers/salesforce.ts +8 -1
  158. package/src/social-providers/slack.ts +15 -9
  159. package/src/social-providers/spotify.ts +8 -1
  160. package/src/social-providers/tiktok.ts +22 -9
  161. package/src/social-providers/twitch.ts +2 -1
  162. package/src/social-providers/twitter.ts +1 -0
  163. package/src/social-providers/vercel.ts +8 -1
  164. package/src/social-providers/vk.ts +8 -1
  165. package/src/social-providers/wechat.ts +9 -1
  166. package/src/social-providers/zoom.ts +15 -19
  167. package/src/types/context.ts +33 -5
  168. package/src/types/init-options.ts +29 -5
  169. package/src/utils/ip.ts +12 -13
  170. package/src/utils/redirect-uri.ts +54 -0
  171. package/src/utils/string.ts +37 -0
  172. package/src/utils/url.ts +28 -0
@@ -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;
@@ -22,7 +22,7 @@ export interface AppleProfile {
22
22
  * The email address is either the user's real email address or the proxy
23
23
  * address, depending on their status private email relay service.
24
24
  */
25
- email: string;
25
+ email?: string;
26
26
  /**
27
27
  * A string or Boolean value that indicates whether the service verifies
28
28
  * the email. The value can either be a string ("true" or "false") or a
@@ -77,12 +77,35 @@ export interface AppleOptions extends ProviderOptions<AppleProfile> {
77
77
  audience?: (string | string[]) | undefined;
78
78
  }
79
79
 
80
+ async function sha256Hex(value: string) {
81
+ const data = new TextEncoder().encode(value);
82
+ const digest = await crypto.subtle.digest("SHA-256", data);
83
+ return Array.from(new Uint8Array(digest))
84
+ .map((byte) => byte.toString(16).padStart(2, "0"))
85
+ .join("");
86
+ }
87
+
88
+ async function nonceMatches(jwtNonce: unknown, nonce: string) {
89
+ if (typeof jwtNonce !== "string") {
90
+ return false;
91
+ }
92
+ if (jwtNonce === nonce) {
93
+ return true;
94
+ }
95
+ return jwtNonce === (await sha256Hex(nonce));
96
+ }
97
+
80
98
  export const apple = (options: AppleOptions) => {
81
99
  const tokenEndpoint = "https://appleid.apple.com/auth/token";
82
100
  return {
83
101
  id: "apple",
84
102
  name: "Apple",
85
- async createAuthorizationURL({ state, scopes, redirectURI }) {
103
+ async createAuthorizationURL({
104
+ state,
105
+ scopes,
106
+ redirectURI,
107
+ additionalParams,
108
+ }) {
86
109
  if (!getPrimaryClientId(options.clientId) || !options.clientSecret) {
87
110
  logger.error(
88
111
  "Client ID and client secret are required for Apple. Make sure to provide them in the options.",
@@ -101,6 +124,7 @@ export const apple = (options: AppleOptions) => {
101
124
  redirectURI,
102
125
  responseMode: "form_post",
103
126
  responseType: "code id_token",
127
+ additionalParams,
104
128
  });
105
129
  return url;
106
130
  },
@@ -141,7 +165,7 @@ export const apple = (options: AppleOptions) => {
141
165
  jwtClaims[field] = Boolean(jwtClaims[field]);
142
166
  }
143
167
  });
144
- if (nonce && jwtClaims.nonce !== nonce) {
168
+ if (nonce && !(await nonceMatches(jwtClaims.nonce, nonce))) {
145
169
  return false;
146
170
  }
147
171
  return !!jwtClaims;
@@ -35,7 +35,13 @@ export const atlassian = (options: AtlassianOptions) => {
35
35
  id: "atlassian",
36
36
  name: "Atlassian",
37
37
 
38
- async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
38
+ async createAuthorizationURL({
39
+ state,
40
+ scopes,
41
+ codeVerifier,
42
+ redirectURI,
43
+ additionalParams,
44
+ }) {
39
45
  if (!options.clientId || !options.clientSecret) {
40
46
  logger.error("Client Id and Secret are required for Atlassian");
41
47
  throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
@@ -59,6 +65,7 @@ export const atlassian = (options: AtlassianOptions) => {
59
65
  codeVerifier,
60
66
  redirectURI,
61
67
  additionalParams: {
68
+ ...(additionalParams ?? {}),
62
69
  audience: "api.atlassian.com",
63
70
  },
64
71
  prompt: options.prompt,
@@ -42,6 +42,19 @@ export interface CognitoOptions extends ProviderOptions<CognitoProfile> {
42
42
  region: string;
43
43
  userPoolId: string;
44
44
  requireClientSecret?: boolean | undefined;
45
+ /**
46
+ * Skip the Cognito hosted-UI identity-provider picker by preselecting an
47
+ * IdP (maps to the `identity_provider` query parameter on the authorize
48
+ * request). Accepts `"COGNITO"`, a SAML/OIDC provider name configured on
49
+ * the User Pool, or one of the social providers (`"Google"`, `"Facebook"`,
50
+ * `"LoginWithAmazon"`, `"SignInWithApple"`).
51
+ *
52
+ * Per-request overrides via `signIn.social({ additionalParams: { identity_provider } })`
53
+ * take precedence over this value.
54
+ *
55
+ * @see https://docs.aws.amazon.com/cognito/latest/developerguide/authorization-endpoint.html
56
+ */
57
+ identityProvider?: string | undefined;
45
58
  }
46
59
 
47
60
  export const cognito = (options: CognitoOptions) => {
@@ -60,7 +73,13 @@ export const cognito = (options: CognitoOptions) => {
60
73
  return {
61
74
  id: "cognito",
62
75
  name: "Cognito",
63
- async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
76
+ async createAuthorizationURL({
77
+ state,
78
+ scopes,
79
+ codeVerifier,
80
+ redirectURI,
81
+ additionalParams,
82
+ }) {
64
83
  if (!getPrimaryClientId(options.clientId)) {
65
84
  logger.error(
66
85
  "ClientId is required for Amazon Cognito. Make sure to provide them in the options.",
@@ -91,6 +110,12 @@ export const cognito = (options: CognitoOptions) => {
91
110
  codeVerifier,
92
111
  redirectURI,
93
112
  prompt: options.prompt,
113
+ additionalParams: {
114
+ ...(options.identityProvider
115
+ ? { identity_provider: options.identityProvider }
116
+ : {}),
117
+ ...(additionalParams ?? {}),
118
+ },
94
119
  });
95
120
  // AWS Cognito requires scopes to be encoded with %20 instead of +
96
121
  // URLSearchParams encodes spaces as + by default, so we need to fix this
@@ -1,6 +1,10 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
2
  import type { OAuthProvider, ProviderOptions } from "../oauth2";
3
- import { refreshAccessToken, validateAuthorizationCode } from "../oauth2";
3
+ import {
4
+ createAuthorizationURL,
5
+ refreshAccessToken,
6
+ validateAuthorizationCode,
7
+ } from "../oauth2";
4
8
  export interface DiscordProfile extends Record<string, any> {
5
9
  /** the user's id (i.e. the numerical snowflake) */
6
10
  id: string;
@@ -41,7 +45,7 @@ export interface DiscordProfile extends Record<string, any> {
41
45
  /** whether the email on this account has been verified */
42
46
  verified: boolean;
43
47
  /** the user's email */
44
- email: string;
48
+ email?: string | null;
45
49
  /**
46
50
  * the flags on a user's account:
47
51
  * https://discord.com/developers/docs/resources/user#user-object-user-flags
@@ -84,26 +88,26 @@ export const discord = (options: DiscordOptions) => {
84
88
  return {
85
89
  id: "discord",
86
90
  name: "Discord",
87
- createAuthorizationURL({ state, scopes, redirectURI }) {
91
+ createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
88
92
  const _scopes = options.disableDefaultScope ? [] : ["identify", "email"];
89
93
  if (scopes) _scopes.push(...scopes);
90
94
  if (options.scope) _scopes.push(...options.scope);
91
95
  const hasBotScope = _scopes.includes("bot");
92
- const permissionsParam =
93
- hasBotScope && options.permissions !== undefined
94
- ? `&permissions=${options.permissions}`
95
- : "";
96
- return new URL(
97
- `https://discord.com/api/oauth2/authorize?scope=${_scopes.join(
98
- "+",
99
- )}&response_type=code&client_id=${
100
- options.clientId
101
- }&redirect_uri=${encodeURIComponent(
102
- options.redirectURI || redirectURI,
103
- )}&state=${state}&prompt=${
104
- options.prompt || "none"
105
- }${permissionsParam}`,
106
- );
96
+ return createAuthorizationURL({
97
+ id: "discord",
98
+ options,
99
+ authorizationEndpoint: "https://discord.com/api/oauth2/authorize",
100
+ scopes: _scopes,
101
+ state,
102
+ redirectURI,
103
+ prompt: options.prompt || "none",
104
+ additionalParams: {
105
+ ...(hasBotScope && options.permissions !== undefined
106
+ ? { permissions: String(options.permissions) }
107
+ : {}),
108
+ ...(additionalParams ?? {}),
109
+ },
110
+ });
107
111
  },
108
112
  validateAuthorizationCode: async ({ code, redirectURI }) => {
109
113
  return validateAuthorizationCode({