@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
@@ -9,13 +9,38 @@ 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
 
17
- /** Last fetched jwks used locally in getJwks @internal */
18
- let jwks: JSONWebKeySet | undefined;
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
+
28
+ interface JwksCacheEntry {
29
+ jwks: JSONWebKeySet;
30
+ fetchedAt: number;
31
+ }
32
+
33
+ const jwksCache = new Map<
34
+ string | (() => Promise<JSONWebKeySet | undefined>),
35
+ JwksCacheEntry
36
+ >();
37
+
38
+ /**
39
+ * How long a cached JWKS is trusted before it is refetched
40
+ *
41
+ * @internal
42
+ */
43
+ const JWKS_CACHE_TTL_MS = 5 * 60 * 1000;
19
44
 
20
45
  export interface VerifyAccessTokenRemote {
21
46
  /** Full url of the introspect endpoint. Should end with `/oauth2/introspect` */
@@ -30,6 +55,20 @@ export interface VerifyAccessTokenRemote {
30
55
  * is also still active.
31
56
  */
32
57
  force?: boolean;
58
+ /**
59
+ * Accept introspection responses that omit the `aud` claim even when a
60
+ * required `audience` is configured in `verifyOptions`.
61
+ *
62
+ * By default verification fails closed: if you configure an `audience` and
63
+ * the introspection response has no `aud` (or a mismatching one), the token
64
+ * is rejected. Some authorization servers legitimately omit `aud` from
65
+ * introspection responses (it is OPTIONAL per RFC 7662 §2.2); only enable
66
+ * this if you trust the issuer to bind the token to this resource through
67
+ * another mechanism, as it skips the audience check in that case.
68
+ *
69
+ * @default false
70
+ */
71
+ allowMissingAudience?: boolean;
33
72
  }
34
73
 
35
74
  /**
@@ -82,11 +121,24 @@ export async function getJwks(
82
121
  throw new Error(error as unknown as string);
83
122
  }
84
123
 
85
- if (!jwtHeaders.kid) throw new Error("Missing jwt kid");
124
+ if (!jwtHeaders.kid) {
125
+ throw new APIError("UNAUTHORIZED", { message: "invalid access token" });
126
+ }
127
+ const kid = jwtHeaders.kid;
128
+
129
+ const cacheKey = opts.jwksFetch;
130
+ const cached = jwksCache.get(cacheKey);
131
+ const isFresh = cached
132
+ ? Date.now() - cached.fetchedAt < JWKS_CACHE_TTL_MS
133
+ : false;
134
+ const hasKid = cached?.jwks.keys.some((jwk) => jwk.kid === kid) ?? false;
86
135
 
87
- // Fetch jwks if not set or has a different kid than the one stored
88
- if (!jwks || !jwks.keys.find((jwk) => jwk.kid === jwtHeaders.kid)) {
89
- jwks =
136
+ // Refetch when this source has no cached set, the cached set has expired, or
137
+ // it does not contain the token's kid (e.g. a newly rotated-in key). The
138
+ // cache is scoped to `cacheKey`, so a token is only ever matched against the
139
+ // key set published by its own source.
140
+ if (!cached || !isFresh || !hasKid) {
141
+ const jwks =
90
142
  typeof opts.jwksFetch === "string"
91
143
  ? await betterFetch<JSONWebKeySet>(opts.jwksFetch, {
92
144
  headers: {
@@ -101,9 +153,11 @@ export async function getJwks(
101
153
  })
102
154
  : await opts.jwksFetch();
103
155
  if (!jwks) throw new Error("No jwks found");
156
+ jwksCache.set(cacheKey, { jwks, fetchedAt: Date.now() });
157
+ return jwks;
104
158
  }
105
159
 
106
- return jwks;
160
+ return cached.jwks;
107
161
  }
108
162
 
109
163
  /**
@@ -137,13 +191,16 @@ export async function verifyAccessToken(
137
191
  if (error instanceof Error) {
138
192
  if (error.name === "TypeError" || error.name === "JWSInvalid") {
139
193
  // likely an opaque token (continue)
140
- } else if (error.name === "JWTExpired") {
194
+ } else if (error instanceof joseErrors.JWTExpired) {
141
195
  throw new APIError("UNAUTHORIZED", {
142
196
  message: "token expired",
143
197
  });
144
- } else if (error.name === "JWTInvalid") {
198
+ } else if (error instanceof joseErrors.JOSEError) {
199
+ if (isJoseInfrastructureError(error)) {
200
+ throw error;
201
+ }
145
202
  throw new APIError("UNAUTHORIZED", {
146
- message: "token invalid",
203
+ message: "invalid access token",
147
204
  });
148
205
  } else {
149
206
  throw error;
@@ -185,13 +242,23 @@ export async function verifyAccessToken(
185
242
  throw new APIError("UNAUTHORIZED", {
186
243
  message: "token inactive",
187
244
  });
188
- // Verifies payload using verify options (token valid through introspect)
245
+ // Verifies payload using verify options (token valid through introspect).
246
+ // Audience is enforced by default: when `verifyOptions.audience` is set
247
+ // but the introspection response omits `aud` (or it mismatches),
248
+ // `UnsecuredJWT.decode` throws and the token is rejected. Otherwise a
249
+ // token issued for a different resource/client on the same issuer would
250
+ // also pass. Only drop the audience check when the caller has explicitly
251
+ // opted in via `remoteVerify.allowMissingAudience`.
189
252
  try {
190
253
  const unsecuredJwt = new UnsecuredJWT(introspect).encode();
191
- const { audience: _audience, ...verifyOptions } = opts.verifyOptions;
192
- const verify = introspect.aud
193
- ? UnsecuredJWT.decode(unsecuredJwt, opts.verifyOptions)
194
- : UnsecuredJWT.decode(unsecuredJwt, verifyOptions);
254
+ const { audience: _audience, ...verifyOptionsNoAudience } =
255
+ opts.verifyOptions;
256
+ const skipAudience =
257
+ !introspect.aud && opts.remoteVerify.allowMissingAudience === true;
258
+ const verify = UnsecuredJWT.decode(
259
+ unsecuredJwt,
260
+ skipAudience ? verifyOptionsNoAudience : opts.verifyOptions,
261
+ );
195
262
  payload = verify.payload;
196
263
  } catch (error) {
197
264
  throw new Error(error as unknown as string);
@@ -1,13 +1,14 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
2
 
3
- import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
3
+ import { decodeJwt, importJWK } from "jose";
4
4
  import { logger } from "../env";
5
5
  import { APIError, BetterAuthError } from "../error";
6
- import type { OAuthProvider, ProviderOptions } from "../oauth2";
6
+ import type { ProviderOptions, UpstreamProvider } from "../oauth2";
7
7
  import {
8
8
  createAuthorizationURL,
9
9
  getPrimaryClientId,
10
10
  refreshAccessToken,
11
+ resolveRequestedScopes,
11
12
  validateAuthorizationCode,
12
13
  } from "../oauth2";
13
14
  export interface AppleProfile {
@@ -77,32 +78,42 @@ export interface AppleOptions extends ProviderOptions<AppleProfile> {
77
78
  audience?: (string | string[]) | undefined;
78
79
  }
79
80
 
81
+ const APPLE_DEFAULT_SCOPES = ["email", "name"];
82
+
80
83
  export const apple = (options: AppleOptions) => {
81
84
  const tokenEndpoint = "https://appleid.apple.com/auth/token";
82
85
  return {
83
86
  id: "apple",
84
87
  name: "Apple",
85
- async createAuthorizationURL({ state, scopes, redirectURI }) {
88
+ callbackPath: "/callback/apple",
89
+ async createAuthorizationURL({
90
+ state,
91
+ scopes,
92
+ redirectURI,
93
+ additionalParams,
94
+ }) {
86
95
  if (!getPrimaryClientId(options.clientId) || !options.clientSecret) {
87
96
  logger.error(
88
97
  "Client ID and client secret are required for Apple. Make sure to provide them in the options.",
89
98
  );
90
99
  throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
91
100
  }
92
- const _scope = options.disableDefaultScope ? [] : ["email", "name"];
93
- if (options.scope) _scope.push(...options.scope);
94
- if (scopes) _scope.push(...scopes);
95
- const url = await createAuthorizationURL({
101
+ const requestedScopes = resolveRequestedScopes(
102
+ options,
103
+ APPLE_DEFAULT_SCOPES,
104
+ scopes,
105
+ );
106
+ return createAuthorizationURL({
96
107
  id: "apple",
97
108
  options,
98
109
  authorizationEndpoint: "https://appleid.apple.com/auth/authorize",
99
- scopes: _scope,
110
+ scopes: requestedScopes,
100
111
  state,
101
112
  redirectURI,
102
113
  responseMode: "form_post",
103
114
  responseType: "code id_token",
115
+ additionalParams,
104
116
  });
105
- return url;
106
117
  },
107
118
  validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
108
119
  return validateAuthorizationCode({
@@ -113,41 +124,17 @@ export const apple = (options: AppleOptions) => {
113
124
  tokenEndpoint,
114
125
  });
115
126
  },
116
- async verifyIdToken(token, nonce) {
117
- if (options.disableIdTokenSignIn) {
118
- return false;
119
- }
120
- if (options.verifyIdToken) {
121
- return options.verifyIdToken(token, nonce);
122
- }
123
- try {
124
- const decodedHeader = decodeProtectedHeader(token);
125
- const { kid, alg: jwtAlg } = decodedHeader;
126
- if (!kid || !jwtAlg) return false;
127
- const publicKey = await getApplePublicKey(kid);
128
- const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
129
- algorithms: [jwtAlg],
130
- issuer: "https://appleid.apple.com",
131
- audience:
132
- options.audience && options.audience.length
133
- ? options.audience
134
- : options.appBundleIdentifier
135
- ? options.appBundleIdentifier
136
- : options.clientId,
137
- maxTokenAge: "1h",
138
- });
139
- ["email_verified", "is_private_email"].forEach((field) => {
140
- if (jwtClaims[field] !== undefined) {
141
- jwtClaims[field] = Boolean(jwtClaims[field]);
142
- }
143
- });
144
- if (nonce && jwtClaims.nonce !== nonce) {
145
- return false;
146
- }
147
- return !!jwtClaims;
148
- } catch {
149
- return false;
150
- }
127
+ idToken: {
128
+ jwks: (header) => getApplePublicKey(header.kid!),
129
+ issuer: "https://appleid.apple.com",
130
+ audience:
131
+ options.audience && options.audience.length
132
+ ? options.audience
133
+ : options.appBundleIdentifier
134
+ ? options.appBundleIdentifier
135
+ : options.clientId,
136
+ maxTokenAge: "1h",
137
+ nonceComparison: "exact-or-sha256",
151
138
  },
152
139
  refreshAccessToken: options.refreshAccessToken
153
140
  ? options.refreshAccessToken
@@ -202,7 +189,7 @@ export const apple = (options: AppleOptions) => {
202
189
  };
203
190
  },
204
191
  options,
205
- } satisfies OAuthProvider<AppleProfile>;
192
+ } satisfies UpstreamProvider<AppleProfile>;
206
193
  };
207
194
 
208
195
  export const getApplePublicKey = async (kid: string) => {
@@ -1,10 +1,11 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
2
  import { logger } from "../env";
3
3
  import { BetterAuthError } from "../error";
4
- import type { OAuthProvider, ProviderOptions } from "../oauth2";
4
+ import type { ProviderOptions, UpstreamProvider } from "../oauth2";
5
5
  import {
6
6
  createAuthorizationURL,
7
7
  refreshAccessToken,
8
+ resolveRequestedScopes,
8
9
  validateAuthorizationCode,
9
10
  } from "../oauth2";
10
11
 
@@ -29,13 +30,22 @@ export interface AtlassianOptions extends ProviderOptions<AtlassianProfile> {
29
30
  clientId: string;
30
31
  }
31
32
 
33
+ const ATLASSIAN_DEFAULT_SCOPES = ["read:jira-user", "offline_access"];
34
+
32
35
  export const atlassian = (options: AtlassianOptions) => {
33
36
  const tokenEndpoint = "https://auth.atlassian.com/oauth/token";
34
37
  return {
35
38
  id: "atlassian",
36
39
  name: "Atlassian",
40
+ callbackPath: "/callback/atlassian",
37
41
 
38
- async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
42
+ async createAuthorizationURL({
43
+ state,
44
+ scopes,
45
+ codeVerifier,
46
+ redirectURI,
47
+ additionalParams,
48
+ }) {
39
49
  if (!options.clientId || !options.clientSecret) {
40
50
  logger.error("Client Id and Secret are required for Atlassian");
41
51
  throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
@@ -44,21 +54,22 @@ export const atlassian = (options: AtlassianOptions) => {
44
54
  throw new BetterAuthError("codeVerifier is required for Atlassian");
45
55
  }
46
56
 
47
- const _scopes = options.disableDefaultScope
48
- ? []
49
- : ["read:jira-user", "offline_access"];
50
- if (options.scope) _scopes.push(...options.scope);
51
- if (scopes) _scopes.push(...scopes);
57
+ const requestedScopes = resolveRequestedScopes(
58
+ options,
59
+ ATLASSIAN_DEFAULT_SCOPES,
60
+ scopes,
61
+ );
52
62
 
53
63
  return createAuthorizationURL({
54
64
  id: "atlassian",
55
65
  options,
56
66
  authorizationEndpoint: "https://auth.atlassian.com/authorize",
57
- scopes: _scopes,
67
+ scopes: requestedScopes,
58
68
  state,
59
69
  codeVerifier,
60
70
  redirectURI,
61
71
  additionalParams: {
72
+ ...(additionalParams ?? {}),
62
73
  audience: "api.atlassian.com",
63
74
  },
64
75
  prompt: options.prompt,
@@ -129,5 +140,5 @@ export const atlassian = (options: AtlassianOptions) => {
129
140
  },
130
141
 
131
142
  options,
132
- } satisfies OAuthProvider<AtlassianProfile>;
143
+ } satisfies UpstreamProvider<AtlassianProfile>;
133
144
  };
@@ -1,12 +1,13 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
- import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
2
+ import { decodeJwt, importJWK } from "jose";
3
3
  import { logger } from "../env";
4
4
  import { APIError, BetterAuthError } from "../error";
5
- import type { OAuthProvider, ProviderOptions } from "../oauth2";
5
+ import type { ProviderOptions, UpstreamProvider } from "../oauth2";
6
6
  import {
7
7
  createAuthorizationURL,
8
8
  getPrimaryClientId,
9
9
  refreshAccessToken,
10
+ resolveRequestedScopes,
10
11
  validateAuthorizationCode,
11
12
  } from "../oauth2";
12
13
 
@@ -42,8 +43,23 @@ export interface CognitoOptions extends ProviderOptions<CognitoProfile> {
42
43
  region: string;
43
44
  userPoolId: string;
44
45
  requireClientSecret?: boolean | undefined;
46
+ /**
47
+ * Skip the Cognito hosted-UI identity-provider picker by preselecting an
48
+ * IdP (maps to the `identity_provider` query parameter on the authorize
49
+ * request). Accepts `"COGNITO"`, a SAML/OIDC provider name configured on
50
+ * the User Pool, or one of the social providers (`"Google"`, `"Facebook"`,
51
+ * `"LoginWithAmazon"`, `"SignInWithApple"`).
52
+ *
53
+ * Per-request overrides via `signIn.social({ additionalParams: { identity_provider } })`
54
+ * take precedence over this value.
55
+ *
56
+ * @see https://docs.aws.amazon.com/cognito/latest/developerguide/authorization-endpoint.html
57
+ */
58
+ identityProvider?: string | undefined;
45
59
  }
46
60
 
61
+ const COGNITO_DEFAULT_SCOPES = ["openid", "profile", "email"];
62
+
47
63
  export const cognito = (options: CognitoOptions) => {
48
64
  if (!options.domain || !options.region || !options.userPoolId) {
49
65
  logger.error(
@@ -60,7 +76,14 @@ export const cognito = (options: CognitoOptions) => {
60
76
  return {
61
77
  id: "cognito",
62
78
  name: "Cognito",
63
- async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
79
+ callbackPath: "/callback/cognito",
80
+ async createAuthorizationURL({
81
+ state,
82
+ scopes,
83
+ codeVerifier,
84
+ redirectURI,
85
+ additionalParams,
86
+ }) {
64
87
  if (!getPrimaryClientId(options.clientId)) {
65
88
  logger.error(
66
89
  "ClientId is required for Amazon Cognito. Make sure to provide them in the options.",
@@ -74,23 +97,29 @@ export const cognito = (options: CognitoOptions) => {
74
97
  );
75
98
  throw new BetterAuthError("CLIENT_SECRET_REQUIRED");
76
99
  }
77
- const _scopes = options.disableDefaultScope
78
- ? []
79
- : ["openid", "profile", "email"];
80
- if (options.scope) _scopes.push(...options.scope);
81
- if (scopes) _scopes.push(...scopes);
100
+ const requestedScopes = resolveRequestedScopes(
101
+ options,
102
+ COGNITO_DEFAULT_SCOPES,
103
+ scopes,
104
+ );
82
105
 
83
- const url = await createAuthorizationURL({
106
+ const { url } = await createAuthorizationURL({
84
107
  id: "cognito",
85
108
  options: {
86
109
  ...options,
87
110
  },
88
111
  authorizationEndpoint,
89
- scopes: _scopes,
112
+ scopes: requestedScopes,
90
113
  state,
91
114
  codeVerifier,
92
115
  redirectURI,
93
116
  prompt: options.prompt,
117
+ additionalParams: {
118
+ ...(options.identityProvider
119
+ ? { identity_provider: options.identityProvider }
120
+ : {}),
121
+ ...(additionalParams ?? {}),
122
+ },
94
123
  });
95
124
  // AWS Cognito requires scopes to be encoded with %20 instead of +
96
125
  // URLSearchParams encodes spaces as + by default, so we need to fix this
@@ -101,9 +130,12 @@ export const cognito = (options: CognitoOptions) => {
101
130
  // Manually append the scope with proper encoding to the URL
102
131
  const urlString = url.toString();
103
132
  const separator = urlString.includes("?") ? "&" : "?";
104
- return new URL(`${urlString}${separator}scope=${encodedScope}`);
133
+ return {
134
+ url: new URL(`${urlString}${separator}scope=${encodedScope}`),
135
+ requestedScopes,
136
+ };
105
137
  }
106
- return url;
138
+ return { url, requestedScopes };
107
139
  },
108
140
 
109
141
  validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
@@ -130,41 +162,12 @@ export const cognito = (options: CognitoOptions) => {
130
162
  });
131
163
  },
132
164
 
133
- async verifyIdToken(token, nonce) {
134
- if (options.disableIdTokenSignIn) {
135
- return false;
136
- }
137
- if (options.verifyIdToken) {
138
- return options.verifyIdToken(token, nonce);
139
- }
140
-
141
- try {
142
- const decodedHeader = decodeProtectedHeader(token);
143
- const { kid, alg: jwtAlg } = decodedHeader;
144
- if (!kid || !jwtAlg) return false;
145
-
146
- const publicKey = await getCognitoPublicKey(
147
- kid,
148
- options.region,
149
- options.userPoolId,
150
- );
151
- const expectedIssuer = `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`;
152
-
153
- const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
154
- algorithms: [jwtAlg],
155
- issuer: expectedIssuer,
156
- audience: options.clientId,
157
- maxTokenAge: "1h",
158
- });
159
-
160
- if (nonce && jwtClaims.nonce !== nonce) {
161
- return false;
162
- }
163
- return true;
164
- } catch (error) {
165
- logger.error("Failed to verify ID token:", error);
166
- return false;
167
- }
165
+ idToken: {
166
+ jwks: (header) =>
167
+ getCognitoPublicKey(header.kid!, options.region, options.userPoolId),
168
+ issuer: `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`,
169
+ audience: options.clientId,
170
+ maxTokenAge: "1h",
168
171
  },
169
172
 
170
173
  async getUserInfo(token) {
@@ -240,7 +243,7 @@ export const cognito = (options: CognitoOptions) => {
240
243
  },
241
244
 
242
245
  options,
243
- } satisfies OAuthProvider<CognitoProfile>;
246
+ } satisfies UpstreamProvider<CognitoProfile>;
244
247
  };
245
248
 
246
249
  export const getCognitoPublicKey = async (
@@ -1,6 +1,11 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
- import type { OAuthProvider, ProviderOptions } from "../oauth2";
3
- import { refreshAccessToken, validateAuthorizationCode } from "../oauth2";
2
+ import type { ProviderOptions, UpstreamProvider } from "../oauth2";
3
+ import {
4
+ createAuthorizationURL,
5
+ refreshAccessToken,
6
+ resolveRequestedScopes,
7
+ validateAuthorizationCode,
8
+ } from "../oauth2";
4
9
  export interface DiscordProfile extends Record<string, any> {
5
10
  /** the user's id (i.e. the numerical snowflake) */
6
11
  id: string;
@@ -79,31 +84,41 @@ export interface DiscordOptions extends ProviderOptions<DiscordProfile> {
79
84
  permissions?: number | undefined;
80
85
  }
81
86
 
87
+ const DISCORD_DEFAULT_SCOPES = ["identify", "email"];
88
+
82
89
  export const discord = (options: DiscordOptions) => {
83
90
  const tokenEndpoint = "https://discord.com/api/oauth2/token";
84
91
  return {
85
92
  id: "discord",
86
93
  name: "Discord",
87
- createAuthorizationURL({ state, scopes, redirectURI }) {
88
- const _scopes = options.disableDefaultScope ? [] : ["identify", "email"];
89
- if (scopes) _scopes.push(...scopes);
90
- if (options.scope) _scopes.push(...options.scope);
91
- 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}`,
94
+ callbackPath: "/callback/discord",
95
+ async createAuthorizationURL({
96
+ state,
97
+ scopes,
98
+ redirectURI,
99
+ additionalParams,
100
+ }) {
101
+ const requestedScopes = resolveRequestedScopes(
102
+ options,
103
+ DISCORD_DEFAULT_SCOPES,
104
+ scopes,
106
105
  );
106
+ const hasBotScope = requestedScopes.includes("bot");
107
+ return createAuthorizationURL({
108
+ id: "discord",
109
+ options,
110
+ authorizationEndpoint: "https://discord.com/api/oauth2/authorize",
111
+ scopes: requestedScopes,
112
+ state,
113
+ redirectURI,
114
+ prompt: options.prompt || "none",
115
+ additionalParams: {
116
+ ...(hasBotScope && options.permissions !== undefined
117
+ ? { permissions: String(options.permissions) }
118
+ : {}),
119
+ ...(additionalParams ?? {}),
120
+ },
121
+ });
107
122
  },
108
123
  validateAuthorizationCode: async ({ code, redirectURI }) => {
109
124
  return validateAuthorizationCode({
@@ -166,5 +181,5 @@ export const discord = (options: DiscordOptions) => {
166
181
  };
167
182
  },
168
183
  options,
169
- } satisfies OAuthProvider<DiscordProfile>;
184
+ } satisfies UpstreamProvider<DiscordProfile>;
170
185
  };
@@ -1,8 +1,9 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
- import type { OAuthProvider, ProviderOptions } from "../oauth2";
2
+ import type { ProviderOptions, UpstreamProvider } from "../oauth2";
3
3
  import {
4
4
  createAuthorizationURL,
5
5
  refreshAccessToken,
6
+ resolveRequestedScopes,
6
7
  validateAuthorizationCode,
7
8
  } from "../oauth2";
8
9
 
@@ -25,34 +26,41 @@ export interface DropboxOptions extends ProviderOptions<DropboxProfile> {
25
26
  accessType?: ("offline" | "online" | "legacy") | undefined;
26
27
  }
27
28
 
29
+ const DROPBOX_DEFAULT_SCOPES = ["account_info.read"];
30
+
28
31
  export const dropbox = (options: DropboxOptions) => {
29
32
  const tokenEndpoint = "https://api.dropboxapi.com/oauth2/token";
30
33
 
31
34
  return {
32
35
  id: "dropbox",
33
36
  name: "Dropbox",
37
+ callbackPath: "/callback/dropbox",
34
38
  createAuthorizationURL: async ({
35
39
  state,
36
40
  scopes,
37
41
  codeVerifier,
38
42
  redirectURI,
43
+ additionalParams,
39
44
  }) => {
40
- const _scopes = options.disableDefaultScope ? [] : ["account_info.read"];
41
- if (options.scope) _scopes.push(...options.scope);
42
- if (scopes) _scopes.push(...scopes);
43
- const additionalParams: Record<string, string> = {};
44
- if (options.accessType) {
45
- additionalParams.token_access_type = options.accessType;
46
- }
47
- return await createAuthorizationURL({
45
+ const requestedScopes = resolveRequestedScopes(
46
+ options,
47
+ DROPBOX_DEFAULT_SCOPES,
48
+ scopes,
49
+ );
50
+ return createAuthorizationURL({
48
51
  id: "dropbox",
49
52
  options,
50
53
  authorizationEndpoint: "https://www.dropbox.com/oauth2/authorize",
51
- scopes: _scopes,
54
+ scopes: requestedScopes,
52
55
  state,
53
56
  redirectURI,
54
57
  codeVerifier,
55
- additionalParams,
58
+ additionalParams: {
59
+ ...(options.accessType
60
+ ? { token_access_type: options.accessType }
61
+ : {}),
62
+ ...(additionalParams ?? {}),
63
+ },
56
64
  });
57
65
  },
58
66
  validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
@@ -108,5 +116,5 @@ export const dropbox = (options: DropboxOptions) => {
108
116
  };
109
117
  },
110
118
  options,
111
- } satisfies OAuthProvider<DropboxProfile>;
119
+ } satisfies UpstreamProvider<DropboxProfile>;
112
120
  };