@better-auth/core 1.5.0-beta.8 → 1.5.0

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 (313) hide show
  1. package/README.md +17 -0
  2. package/dist/api/index.d.mts +144 -41
  3. package/dist/api/index.mjs +2 -1
  4. package/dist/api/index.mjs.map +1 -0
  5. package/dist/async_hooks/index.d.mts +2 -1
  6. package/dist/async_hooks/index.mjs +2 -1
  7. package/dist/async_hooks/index.mjs.map +1 -0
  8. package/dist/async_hooks/pure.index.d.mts +2 -1
  9. package/dist/async_hooks/pure.index.mjs +2 -1
  10. package/dist/async_hooks/pure.index.mjs.map +1 -0
  11. package/dist/context/endpoint-context.d.mts +2 -1
  12. package/dist/context/endpoint-context.mjs +4 -3
  13. package/dist/context/endpoint-context.mjs.map +1 -0
  14. package/dist/context/global.d.mts +2 -2
  15. package/dist/context/global.mjs +3 -2
  16. package/dist/context/global.mjs.map +1 -0
  17. package/dist/context/index.d.mts +2 -2
  18. package/dist/context/index.mjs +2 -2
  19. package/dist/context/request-state.d.mts +2 -1
  20. package/dist/context/request-state.mjs +4 -3
  21. package/dist/context/request-state.mjs.map +1 -0
  22. package/dist/context/transaction.d.mts +12 -3
  23. package/dist/context/transaction.mjs +55 -11
  24. package/dist/context/transaction.mjs.map +1 -0
  25. package/dist/db/adapter/factory.d.mts +6 -13
  26. package/dist/db/adapter/factory.mjs +44 -57
  27. package/dist/db/adapter/factory.mjs.map +1 -0
  28. package/dist/db/adapter/get-default-field-name.d.mts +2 -1
  29. package/dist/db/adapter/get-default-field-name.mjs +3 -2
  30. package/dist/db/adapter/get-default-field-name.mjs.map +1 -0
  31. package/dist/db/adapter/get-default-model-name.d.mts +2 -1
  32. package/dist/db/adapter/get-default-model-name.mjs +5 -4
  33. package/dist/db/adapter/get-default-model-name.mjs.map +1 -0
  34. package/dist/db/adapter/get-field-attributes.d.mts +3 -2
  35. package/dist/db/adapter/get-field-attributes.mjs +2 -1
  36. package/dist/db/adapter/get-field-attributes.mjs.map +1 -0
  37. package/dist/db/adapter/get-field-name.d.mts +2 -1
  38. package/dist/db/adapter/get-field-name.mjs +2 -1
  39. package/dist/db/adapter/get-field-name.mjs.map +1 -0
  40. package/dist/db/adapter/get-id-field.d.mts +3 -2
  41. package/dist/db/adapter/get-id-field.mjs +3 -2
  42. package/dist/db/adapter/get-id-field.mjs.map +1 -0
  43. package/dist/db/adapter/get-model-name.d.mts +2 -1
  44. package/dist/db/adapter/get-model-name.mjs +2 -1
  45. package/dist/db/adapter/get-model-name.mjs.map +1 -0
  46. package/dist/db/adapter/index.d.mts +10 -4
  47. package/dist/db/adapter/index.mjs +19 -2
  48. package/dist/db/adapter/index.mjs.map +1 -0
  49. package/dist/db/adapter/types.d.mts +3 -34
  50. package/dist/db/adapter/utils.d.mts +2 -1
  51. package/dist/db/adapter/utils.mjs +2 -1
  52. package/dist/db/adapter/utils.mjs.map +1 -0
  53. package/dist/db/get-tables.d.mts +2 -1
  54. package/dist/db/get-tables.mjs +46 -39
  55. package/dist/db/get-tables.mjs.map +1 -0
  56. package/dist/db/index.d.mts +7 -7
  57. package/dist/db/plugin.d.mts +2 -1
  58. package/dist/db/schema/account.d.mts +8 -4
  59. package/dist/db/schema/account.mjs +2 -1
  60. package/dist/db/schema/account.mjs.map +1 -0
  61. package/dist/db/schema/rate-limit.d.mts +8 -2
  62. package/dist/db/schema/rate-limit.mjs +2 -1
  63. package/dist/db/schema/rate-limit.mjs.map +1 -0
  64. package/dist/db/schema/session.d.mts +8 -4
  65. package/dist/db/schema/session.mjs +2 -1
  66. package/dist/db/schema/session.mjs.map +1 -0
  67. package/dist/db/schema/shared.d.mts +2 -1
  68. package/dist/db/schema/shared.mjs +2 -1
  69. package/dist/db/schema/shared.mjs.map +1 -0
  70. package/dist/db/schema/user.d.mts +8 -4
  71. package/dist/db/schema/user.mjs +2 -1
  72. package/dist/db/schema/user.mjs.map +1 -0
  73. package/dist/db/schema/verification.d.mts +8 -4
  74. package/dist/db/schema/verification.mjs +2 -1
  75. package/dist/db/schema/verification.mjs.map +1 -0
  76. package/dist/db/type.d.mts +28 -2
  77. package/dist/env/color-depth.d.mts +2 -1
  78. package/dist/env/color-depth.mjs +2 -1
  79. package/dist/env/color-depth.mjs.map +1 -0
  80. package/dist/env/env-impl.d.mts +3 -2
  81. package/dist/env/env-impl.mjs +9 -8
  82. package/dist/env/env-impl.mjs.map +1 -0
  83. package/dist/env/logger.d.mts +2 -1
  84. package/dist/env/logger.mjs +3 -2
  85. package/dist/env/logger.mjs.map +1 -0
  86. package/dist/error/codes.d.mts +64 -181
  87. package/dist/error/codes.mjs +6 -2
  88. package/dist/error/codes.mjs.map +1 -0
  89. package/dist/error/index.d.mts +2 -1
  90. package/dist/error/index.mjs +2 -1
  91. package/dist/error/index.mjs.map +1 -0
  92. package/dist/index.d.mts +5 -4
  93. package/dist/oauth2/client-credentials-token.d.mts +25 -3
  94. package/dist/oauth2/client-credentials-token.mjs +15 -2
  95. package/dist/oauth2/client-credentials-token.mjs.map +1 -0
  96. package/dist/oauth2/create-authorization-url.d.mts +5 -2
  97. package/dist/oauth2/create-authorization-url.mjs +3 -1
  98. package/dist/oauth2/create-authorization-url.mjs.map +1 -0
  99. package/dist/oauth2/index.d.mts +4 -4
  100. package/dist/oauth2/index.mjs +4 -4
  101. package/dist/oauth2/oauth-provider.d.mts +3 -2
  102. package/dist/oauth2/refresh-access-token.d.mts +24 -4
  103. package/dist/oauth2/refresh-access-token.mjs +20 -2
  104. package/dist/oauth2/refresh-access-token.mjs.map +1 -0
  105. package/dist/oauth2/utils.d.mts +2 -1
  106. package/dist/oauth2/utils.mjs +2 -1
  107. package/dist/oauth2/utils.mjs.map +1 -0
  108. package/dist/oauth2/validate-authorization-code.d.mts +37 -4
  109. package/dist/oauth2/validate-authorization-code.mjs +25 -13
  110. package/dist/oauth2/validate-authorization-code.mjs.map +1 -0
  111. package/dist/oauth2/verify.d.mts +7 -13
  112. package/dist/oauth2/verify.mjs +2 -1
  113. package/dist/oauth2/verify.mjs.map +1 -0
  114. package/dist/social-providers/apple.d.mts +2 -1
  115. package/dist/social-providers/apple.mjs +22 -21
  116. package/dist/social-providers/apple.mjs.map +1 -0
  117. package/dist/social-providers/atlassian.d.mts +2 -1
  118. package/dist/social-providers/atlassian.mjs +2 -1
  119. package/dist/social-providers/atlassian.mjs.map +1 -0
  120. package/dist/social-providers/cognito.d.mts +2 -1
  121. package/dist/social-providers/cognito.mjs +4 -3
  122. package/dist/social-providers/cognito.mjs.map +1 -0
  123. package/dist/social-providers/discord.d.mts +2 -1
  124. package/dist/social-providers/discord.mjs +2 -1
  125. package/dist/social-providers/discord.mjs.map +1 -0
  126. package/dist/social-providers/dropbox.d.mts +2 -1
  127. package/dist/social-providers/dropbox.mjs +3 -2
  128. package/dist/social-providers/dropbox.mjs.map +1 -0
  129. package/dist/social-providers/facebook.d.mts +2 -1
  130. package/dist/social-providers/facebook.mjs +13 -12
  131. package/dist/social-providers/facebook.mjs.map +1 -0
  132. package/dist/social-providers/figma.d.mts +2 -1
  133. package/dist/social-providers/figma.mjs +2 -1
  134. package/dist/social-providers/figma.mjs.map +1 -0
  135. package/dist/social-providers/github.d.mts +3 -2
  136. package/dist/social-providers/github.mjs +23 -6
  137. package/dist/social-providers/github.mjs.map +1 -0
  138. package/dist/social-providers/gitlab.d.mts +2 -1
  139. package/dist/social-providers/gitlab.mjs +3 -2
  140. package/dist/social-providers/gitlab.mjs.map +1 -0
  141. package/dist/social-providers/google.d.mts +2 -1
  142. package/dist/social-providers/google.mjs +18 -13
  143. package/dist/social-providers/google.mjs.map +1 -0
  144. package/dist/social-providers/huggingface.d.mts +2 -1
  145. package/dist/social-providers/huggingface.mjs +3 -2
  146. package/dist/social-providers/huggingface.mjs.map +1 -0
  147. package/dist/social-providers/index.d.mts +61 -8
  148. package/dist/social-providers/index.mjs +5 -2
  149. package/dist/social-providers/index.mjs.map +1 -0
  150. package/dist/social-providers/kakao.d.mts +3 -2
  151. package/dist/social-providers/kakao.mjs +3 -2
  152. package/dist/social-providers/kakao.mjs.map +1 -0
  153. package/dist/social-providers/kick.d.mts +2 -1
  154. package/dist/social-providers/kick.mjs +2 -1
  155. package/dist/social-providers/kick.mjs.map +1 -0
  156. package/dist/social-providers/line.d.mts +2 -1
  157. package/dist/social-providers/line.mjs +3 -2
  158. package/dist/social-providers/line.mjs.map +1 -0
  159. package/dist/social-providers/linear.d.mts +2 -1
  160. package/dist/social-providers/linear.mjs +2 -1
  161. package/dist/social-providers/linear.mjs.map +1 -0
  162. package/dist/social-providers/linkedin.d.mts +2 -1
  163. package/dist/social-providers/linkedin.mjs +2 -1
  164. package/dist/social-providers/linkedin.mjs.map +1 -0
  165. package/dist/social-providers/microsoft-entra-id.d.mts +4 -1
  166. package/dist/social-providers/microsoft-entra-id.mjs +36 -2
  167. package/dist/social-providers/microsoft-entra-id.mjs.map +1 -0
  168. package/dist/social-providers/naver.d.mts +11 -20
  169. package/dist/social-providers/naver.mjs +3 -2
  170. package/dist/social-providers/naver.mjs.map +1 -0
  171. package/dist/social-providers/notion.d.mts +2 -1
  172. package/dist/social-providers/notion.mjs +3 -2
  173. package/dist/social-providers/notion.mjs.map +1 -0
  174. package/dist/social-providers/paybin.d.mts +2 -1
  175. package/dist/social-providers/paybin.mjs +3 -2
  176. package/dist/social-providers/paybin.mjs.map +1 -0
  177. package/dist/social-providers/paypal.d.mts +2 -1
  178. package/dist/social-providers/paypal.mjs +2 -1
  179. package/dist/social-providers/paypal.mjs.map +1 -0
  180. package/dist/social-providers/polar.d.mts +2 -1
  181. package/dist/social-providers/polar.mjs +3 -2
  182. package/dist/social-providers/polar.mjs.map +1 -0
  183. package/dist/social-providers/railway.d.mts +68 -0
  184. package/dist/social-providers/railway.mjs +78 -0
  185. package/dist/social-providers/railway.mjs.map +1 -0
  186. package/dist/social-providers/reddit.d.mts +2 -1
  187. package/dist/social-providers/reddit.mjs +2 -1
  188. package/dist/social-providers/reddit.mjs.map +1 -0
  189. package/dist/social-providers/roblox.d.mts +2 -1
  190. package/dist/social-providers/roblox.mjs +2 -1
  191. package/dist/social-providers/roblox.mjs.map +1 -0
  192. package/dist/social-providers/salesforce.d.mts +2 -1
  193. package/dist/social-providers/salesforce.mjs +2 -1
  194. package/dist/social-providers/salesforce.mjs.map +1 -0
  195. package/dist/social-providers/slack.d.mts +2 -1
  196. package/dist/social-providers/slack.mjs +2 -1
  197. package/dist/social-providers/slack.mjs.map +1 -0
  198. package/dist/social-providers/spotify.d.mts +2 -1
  199. package/dist/social-providers/spotify.mjs +2 -1
  200. package/dist/social-providers/spotify.mjs.map +1 -0
  201. package/dist/social-providers/tiktok.d.mts +3 -3
  202. package/dist/social-providers/tiktok.mjs +3 -2
  203. package/dist/social-providers/tiktok.mjs.map +1 -0
  204. package/dist/social-providers/twitch.d.mts +2 -1
  205. package/dist/social-providers/twitch.mjs +2 -1
  206. package/dist/social-providers/twitch.mjs.map +1 -0
  207. package/dist/social-providers/twitter.d.mts +14 -25
  208. package/dist/social-providers/twitter.mjs +2 -1
  209. package/dist/social-providers/twitter.mjs.map +1 -0
  210. package/dist/social-providers/vercel.d.mts +2 -1
  211. package/dist/social-providers/vercel.mjs +3 -2
  212. package/dist/social-providers/vercel.mjs.map +1 -0
  213. package/dist/social-providers/vk.d.mts +2 -1
  214. package/dist/social-providers/vk.mjs +2 -1
  215. package/dist/social-providers/vk.mjs.map +1 -0
  216. package/dist/social-providers/zoom.d.mts +3 -10
  217. package/dist/social-providers/zoom.mjs +2 -1
  218. package/dist/social-providers/zoom.mjs.map +1 -0
  219. package/dist/types/context.d.mts +54 -21
  220. package/dist/types/cookie.d.mts +2 -1
  221. package/dist/types/helper.d.mts +4 -1
  222. package/dist/types/index.d.mts +4 -3
  223. package/dist/types/init-options.d.mts +235 -144
  224. package/dist/types/plugin-client.d.mts +4 -1
  225. package/dist/types/plugin.d.mts +12 -11
  226. package/dist/types/secret.d.mts +12 -0
  227. package/dist/utils/db.d.mts +12 -0
  228. package/dist/utils/db.mjs +17 -0
  229. package/dist/utils/db.mjs.map +1 -0
  230. package/dist/utils/deprecate.d.mts +2 -2
  231. package/dist/utils/deprecate.mjs +2 -1
  232. package/dist/utils/deprecate.mjs.map +1 -0
  233. package/dist/utils/error-codes.d.mts +8 -6
  234. package/dist/utils/error-codes.mjs +3 -2
  235. package/dist/utils/error-codes.mjs.map +1 -0
  236. package/dist/utils/id.d.mts +2 -1
  237. package/dist/utils/id.mjs +2 -1
  238. package/dist/utils/id.mjs.map +1 -0
  239. package/dist/utils/ip.d.mts +55 -0
  240. package/dist/utils/ip.mjs +119 -0
  241. package/dist/utils/ip.mjs.map +1 -0
  242. package/dist/utils/json.d.mts +2 -1
  243. package/dist/utils/json.mjs +2 -1
  244. package/dist/utils/json.mjs.map +1 -0
  245. package/dist/utils/string.d.mts +2 -1
  246. package/dist/utils/string.mjs +2 -1
  247. package/dist/utils/string.mjs.map +1 -0
  248. package/dist/utils/url.d.mts +2 -1
  249. package/dist/utils/url.mjs +2 -1
  250. package/dist/utils/url.mjs.map +1 -0
  251. package/package.json +35 -13
  252. package/src/context/index.ts +1 -0
  253. package/src/context/transaction.ts +72 -9
  254. package/src/db/adapter/factory.ts +41 -73
  255. package/src/db/adapter/get-id-field.ts +1 -3
  256. package/src/db/adapter/index.ts +20 -15
  257. package/src/db/adapter/types.ts +2 -41
  258. package/src/db/get-tables.ts +48 -37
  259. package/src/db/index.ts +30 -5
  260. package/src/db/schema/account.ts +16 -3
  261. package/src/db/schema/rate-limit.ts +16 -1
  262. package/src/db/schema/session.ts +15 -3
  263. package/src/db/schema/user.ts +15 -3
  264. package/src/db/schema/verification.ts +16 -3
  265. package/src/db/test/get-tables.test.ts +33 -0
  266. package/src/db/type.ts +154 -1
  267. package/src/env/env-impl.ts +2 -2
  268. package/src/env/logger.ts +1 -1
  269. package/src/error/codes.ts +17 -0
  270. package/src/oauth2/client-credentials-token.ts +26 -2
  271. package/src/oauth2/create-authorization-url.ts +3 -1
  272. package/src/oauth2/index.ts +3 -0
  273. package/src/oauth2/oauth-provider.ts +1 -1
  274. package/src/oauth2/refresh-access-token.test.ts +90 -0
  275. package/src/oauth2/refresh-access-token.ts +37 -4
  276. package/src/oauth2/validate-authorization-code.ts +55 -29
  277. package/src/oauth2/validate-token.test.ts +229 -0
  278. package/src/social-providers/apple.ts +29 -29
  279. package/src/social-providers/cognito.ts +6 -5
  280. package/src/social-providers/dropbox.ts +1 -1
  281. package/src/social-providers/facebook.ts +3 -3
  282. package/src/social-providers/github.ts +26 -4
  283. package/src/social-providers/gitlab.ts +1 -1
  284. package/src/social-providers/google.ts +18 -14
  285. package/src/social-providers/huggingface.ts +1 -1
  286. package/src/social-providers/index.ts +9 -5
  287. package/src/social-providers/kakao.ts +1 -1
  288. package/src/social-providers/line.ts +1 -1
  289. package/src/social-providers/microsoft-entra-id.ts +84 -1
  290. package/src/social-providers/naver.ts +1 -1
  291. package/src/social-providers/notion.ts +1 -1
  292. package/src/social-providers/paybin.ts +1 -5
  293. package/src/social-providers/polar.ts +1 -1
  294. package/src/social-providers/railway.ts +100 -0
  295. package/src/social-providers/tiktok.ts +2 -1
  296. package/src/social-providers/vercel.ts +1 -1
  297. package/src/social-providers/zoom.ts +0 -8
  298. package/src/types/context.ts +79 -15
  299. package/src/types/helper.ts +9 -0
  300. package/src/types/index.ts +14 -2
  301. package/src/types/init-options.ts +298 -171
  302. package/src/types/plugin-client.ts +1 -0
  303. package/src/types/plugin.ts +11 -6
  304. package/src/types/secret.ts +8 -0
  305. package/src/utils/db.ts +20 -0
  306. package/src/utils/deprecate.test.ts +0 -1
  307. package/src/utils/error-codes.ts +12 -9
  308. package/src/utils/ip.test.ts +255 -0
  309. package/src/utils/ip.ts +211 -0
  310. package/.turbo/turbo-build.log +0 -180
  311. package/tsconfig.json +0 -7
  312. package/tsdown.config.ts +0 -32
  313. package/vitest.config.ts +0 -3
@@ -0,0 +1,229 @@
1
+ import type { JWK } from "jose";
2
+ import { exportJWK, generateKeyPair, SignJWT } from "jose";
3
+ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
4
+ import { validateToken } from "./validate-authorization-code";
5
+
6
+ describe("validateToken", () => {
7
+ const originalFetch = globalThis.fetch;
8
+ const mockedFetch = vi.fn() as unknown as typeof fetch &
9
+ ReturnType<typeof vi.fn>;
10
+
11
+ beforeAll(() => {
12
+ globalThis.fetch = mockedFetch;
13
+ });
14
+
15
+ afterAll(() => {
16
+ globalThis.fetch = originalFetch;
17
+ });
18
+
19
+ async function createTestJWKS(alg: string, crv?: string) {
20
+ const { publicKey, privateKey } = await generateKeyPair(alg, {
21
+ crv,
22
+ extractable: true,
23
+ });
24
+ const publicJWK = await exportJWK(publicKey);
25
+ const privateJWK = await exportJWK(privateKey);
26
+ const kid = `test-key-${Date.now()}`;
27
+ publicJWK.kid = kid;
28
+ publicJWK.alg = alg;
29
+ privateJWK.kid = kid;
30
+ privateJWK.alg = alg;
31
+ return { publicJWK, privateJWK, kid, publicKey, privateKey };
32
+ }
33
+
34
+ async function createSignedToken(
35
+ privateKey: CryptoKey,
36
+ alg: string,
37
+ kid: string,
38
+ payload: Record<string, unknown> = {},
39
+ ) {
40
+ return await new SignJWT({
41
+ sub: "user-123",
42
+ email: "test@example.com",
43
+ iss: "https://example.com",
44
+ aud: "test-client",
45
+ ...payload,
46
+ })
47
+ .setProtectedHeader({ alg, kid })
48
+ .setIssuedAt()
49
+ .setExpirationTime("1h")
50
+ .sign(privateKey);
51
+ }
52
+
53
+ function mockJWKSResponse(...publicJWKs: JWK[]) {
54
+ mockedFetch.mockResolvedValueOnce(
55
+ new Response(JSON.stringify({ keys: publicJWKs }), {
56
+ status: 200,
57
+ headers: { "content-type": "application/json" },
58
+ }),
59
+ );
60
+ }
61
+
62
+ it("should verify RS256 signed token", async () => {
63
+ const { publicJWK, privateKey, kid } = await createTestJWKS("RS256");
64
+ const token = await createSignedToken(privateKey, "RS256", kid);
65
+ mockJWKSResponse(publicJWK);
66
+
67
+ const result = await validateToken(
68
+ token,
69
+ "https://example.com/.well-known/jwks",
70
+ );
71
+
72
+ expect(result).toBeDefined();
73
+ expect(result.payload.sub).toBe("user-123");
74
+ expect(result.payload.email).toBe("test@example.com");
75
+ });
76
+
77
+ it("should verify ES256 signed token", async () => {
78
+ const { publicJWK, privateKey, kid } = await createTestJWKS("ES256");
79
+ const token = await createSignedToken(privateKey, "ES256", kid);
80
+ mockJWKSResponse(publicJWK);
81
+
82
+ const result = await validateToken(
83
+ token,
84
+ "https://example.com/.well-known/jwks",
85
+ );
86
+
87
+ expect(result).toBeDefined();
88
+ expect(result.payload.sub).toBe("user-123");
89
+ });
90
+
91
+ it("should verify EdDSA (Ed25519) signed token", async () => {
92
+ const { publicJWK, privateKey, kid } = await createTestJWKS(
93
+ "EdDSA",
94
+ "Ed25519",
95
+ );
96
+ const token = await createSignedToken(privateKey, "EdDSA", kid);
97
+ mockJWKSResponse(publicJWK);
98
+
99
+ const result = await validateToken(
100
+ token,
101
+ "https://example.com/.well-known/jwks",
102
+ );
103
+
104
+ expect(result).toBeDefined();
105
+ expect(result.payload.sub).toBe("user-123");
106
+ });
107
+
108
+ it("should throw when kid doesn't match any key", async () => {
109
+ const { publicJWK, privateKey } = await createTestJWKS("RS256");
110
+ publicJWK.kid = "different-kid";
111
+ const token = await createSignedToken(privateKey, "RS256", "original-kid");
112
+ mockJWKSResponse(publicJWK);
113
+
114
+ await expect(
115
+ validateToken(token, "https://example.com/.well-known/jwks"),
116
+ ).rejects.toThrow();
117
+ });
118
+
119
+ it("should find correct key when multiple keys exist", async () => {
120
+ const key1 = await createTestJWKS("RS256");
121
+ const key2 = await createTestJWKS("RS256");
122
+ const key3 = await createTestJWKS("ES256");
123
+ const token = await createSignedToken(key2.privateKey, "RS256", key2.kid);
124
+ mockJWKSResponse(key1.publicJWK, key2.publicJWK, key3.publicJWK);
125
+
126
+ const result = await validateToken(
127
+ token,
128
+ "https://example.com/.well-known/jwks",
129
+ );
130
+
131
+ expect(result).toBeDefined();
132
+ expect(result.payload.sub).toBe("user-123");
133
+ });
134
+
135
+ it("should throw when JWKS returns empty keys array", async () => {
136
+ const { privateKey, kid } = await createTestJWKS("RS256");
137
+ const token = await createSignedToken(privateKey, "RS256", kid);
138
+ mockJWKSResponse();
139
+
140
+ await expect(
141
+ validateToken(token, "https://example.com/.well-known/jwks"),
142
+ ).rejects.toThrow();
143
+ });
144
+
145
+ it("should throw when JWKS fetch fails", async () => {
146
+ const { privateKey, kid } = await createTestJWKS("RS256");
147
+ const token = await createSignedToken(privateKey, "RS256", kid);
148
+ mockedFetch.mockResolvedValueOnce(
149
+ new Response("Internal Server Error", { status: 500 }),
150
+ );
151
+
152
+ await expect(
153
+ validateToken(token, "https://example.com/.well-known/jwks"),
154
+ ).rejects.toBeDefined();
155
+ });
156
+
157
+ it("should verify token with matching audience", async () => {
158
+ const { publicJWK, privateKey, kid } = await createTestJWKS("RS256");
159
+ const token = await createSignedToken(privateKey, "RS256", kid);
160
+ mockJWKSResponse(publicJWK);
161
+
162
+ const result = await validateToken(
163
+ token,
164
+ "https://example.com/.well-known/jwks",
165
+ { audience: "test-client" },
166
+ );
167
+
168
+ expect(result).toBeDefined();
169
+ expect(result.payload.aud).toBe("test-client");
170
+ });
171
+
172
+ it("should reject token with mismatched audience", async () => {
173
+ const { publicJWK, privateKey, kid } = await createTestJWKS("RS256");
174
+ const token = await createSignedToken(privateKey, "RS256", kid);
175
+ mockJWKSResponse(publicJWK);
176
+
177
+ await expect(
178
+ validateToken(token, "https://example.com/.well-known/jwks", {
179
+ audience: "wrong-client",
180
+ }),
181
+ ).rejects.toThrow();
182
+ });
183
+
184
+ it("should verify token with matching issuer", async () => {
185
+ const { publicJWK, privateKey, kid } = await createTestJWKS("RS256");
186
+ const token = await createSignedToken(privateKey, "RS256", kid);
187
+ mockJWKSResponse(publicJWK);
188
+
189
+ const result = await validateToken(
190
+ token,
191
+ "https://example.com/.well-known/jwks",
192
+ { issuer: "https://example.com" },
193
+ );
194
+
195
+ expect(result).toBeDefined();
196
+ expect(result.payload.iss).toBe("https://example.com");
197
+ });
198
+
199
+ it("should reject token with mismatched issuer", async () => {
200
+ const { publicJWK, privateKey, kid } = await createTestJWKS("RS256");
201
+ const token = await createSignedToken(privateKey, "RS256", kid);
202
+ mockJWKSResponse(publicJWK);
203
+
204
+ await expect(
205
+ validateToken(token, "https://example.com/.well-known/jwks", {
206
+ issuer: "https://wrong-issuer.com",
207
+ }),
208
+ ).rejects.toThrow();
209
+ });
210
+
211
+ it("should verify token with both audience and issuer", async () => {
212
+ const { publicJWK, privateKey, kid } = await createTestJWKS("RS256");
213
+ const token = await createSignedToken(privateKey, "RS256", kid);
214
+ mockJWKSResponse(publicJWK);
215
+
216
+ const result = await validateToken(
217
+ token,
218
+ "https://example.com/.well-known/jwks",
219
+ {
220
+ audience: "test-client",
221
+ issuer: "https://example.com",
222
+ },
223
+ );
224
+
225
+ expect(result).toBeDefined();
226
+ expect(result.payload.aud).toBe("test-client");
227
+ expect(result.payload.iss).toBe("https://example.com");
228
+ });
229
+ });
@@ -112,41 +112,41 @@ export const apple = (options: AppleOptions) => {
112
112
  if (options.verifyIdToken) {
113
113
  return options.verifyIdToken(token, nonce);
114
114
  }
115
- const decodedHeader = decodeProtectedHeader(token);
116
- const { kid, alg: jwtAlg } = decodedHeader;
117
- if (!kid || !jwtAlg) return false;
118
- const publicKey = await getApplePublicKey(kid);
119
- const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
120
- algorithms: [jwtAlg],
121
- issuer: "https://appleid.apple.com",
122
- audience:
123
- options.audience && options.audience.length
124
- ? options.audience
125
- : options.appBundleIdentifier
126
- ? options.appBundleIdentifier
127
- : options.clientId,
128
- maxTokenAge: "1h",
129
- });
130
- ["email_verified", "is_private_email"].forEach((field) => {
131
- if (jwtClaims[field] !== undefined) {
132
- jwtClaims[field] = Boolean(jwtClaims[field]);
115
+ try {
116
+ const decodedHeader = decodeProtectedHeader(token);
117
+ const { kid, alg: jwtAlg } = decodedHeader;
118
+ if (!kid || !jwtAlg) return false;
119
+ const publicKey = await getApplePublicKey(kid);
120
+ const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
121
+ algorithms: [jwtAlg],
122
+ issuer: "https://appleid.apple.com",
123
+ audience:
124
+ options.audience && options.audience.length
125
+ ? options.audience
126
+ : options.appBundleIdentifier
127
+ ? options.appBundleIdentifier
128
+ : options.clientId,
129
+ maxTokenAge: "1h",
130
+ });
131
+ ["email_verified", "is_private_email"].forEach((field) => {
132
+ if (jwtClaims[field] !== undefined) {
133
+ jwtClaims[field] = Boolean(jwtClaims[field]);
134
+ }
135
+ });
136
+ if (nonce && jwtClaims.nonce !== nonce) {
137
+ return false;
133
138
  }
134
- });
135
- if (nonce && jwtClaims.nonce !== nonce) {
139
+ return !!jwtClaims;
140
+ } catch {
136
141
  return false;
137
142
  }
138
- return !!jwtClaims;
139
143
  },
140
144
  refreshAccessToken: options.refreshAccessToken
141
145
  ? options.refreshAccessToken
142
146
  : async (refreshToken) => {
143
147
  return refreshAccessToken({
144
148
  refreshToken,
145
- options: {
146
- clientId: options.clientId,
147
- clientKey: options.clientKey,
148
- clientSecret: options.clientSecret,
149
- },
149
+ options,
150
150
  tokenEndpoint: "https://appleid.apple.com/auth/token",
151
151
  });
152
152
  },
@@ -162,15 +162,15 @@ export const apple = (options: AppleOptions) => {
162
162
  return null;
163
163
  }
164
164
 
165
- // TODO: " " masking will be removed when the name field is made optional
165
+ // TODO: "" masking will be removed when the name field is made optional
166
166
  let name: string;
167
167
  if (token.user?.name) {
168
168
  const firstName = token.user.name.firstName || "";
169
169
  const lastName = token.user.name.lastName || "";
170
170
  const fullName = `${firstName} ${lastName}`.trim();
171
- name = fullName || " ";
171
+ name = fullName;
172
172
  } else {
173
- name = profile.name || " ";
173
+ name = profile.name || "";
174
174
  }
175
175
 
176
176
  const emailVerified =
@@ -178,10 +178,7 @@ export const cognito = (options: CognitoOptions) => {
178
178
  return null;
179
179
  }
180
180
  const name =
181
- profile.name ||
182
- profile.given_name ||
183
- profile.username ||
184
- profile.email;
181
+ profile.name || profile.given_name || profile.username || "";
185
182
  const enrichedProfile = {
186
183
  ...profile,
187
184
  name,
@@ -220,7 +217,11 @@ export const cognito = (options: CognitoOptions) => {
220
217
  return {
221
218
  user: {
222
219
  id: userInfo.sub,
223
- name: userInfo.name || userInfo.given_name || userInfo.username,
220
+ name:
221
+ userInfo.name ||
222
+ userInfo.given_name ||
223
+ userInfo.username ||
224
+ "",
224
225
  email: userInfo.email,
225
226
  image: userInfo.picture,
226
227
  emailVerified: userInfo.email_verified,
@@ -74,7 +74,7 @@ export const dropbox = (options: DropboxOptions) => {
74
74
  clientKey: options.clientKey,
75
75
  clientSecret: options.clientSecret,
76
76
  },
77
- tokenEndpoint: "https://api.dropbox.com/oauth2/token",
77
+ tokenEndpoint,
78
78
  });
79
79
  },
80
80
  async getUserInfo(token) {
@@ -49,7 +49,7 @@ export const facebook = (options: FacebookOptions) => {
49
49
  return await createAuthorizationURL({
50
50
  id: "facebook",
51
51
  options,
52
- authorizationEndpoint: "https://www.facebook.com/v21.0/dialog/oauth",
52
+ authorizationEndpoint: "https://www.facebook.com/v24.0/dialog/oauth",
53
53
  scopes: _scopes,
54
54
  state,
55
55
  redirectURI,
@@ -66,7 +66,7 @@ export const facebook = (options: FacebookOptions) => {
66
66
  code,
67
67
  redirectURI,
68
68
  options,
69
- tokenEndpoint: "https://graph.facebook.com/oauth/access_token",
69
+ tokenEndpoint: "https://graph.facebook.com/v24.0/oauth/access_token",
70
70
  });
71
71
  },
72
72
  async verifyIdToken(token, nonce) {
@@ -121,7 +121,7 @@ export const facebook = (options: FacebookOptions) => {
121
121
  clientSecret: options.clientSecret,
122
122
  },
123
123
  tokenEndpoint:
124
- "https://graph.facebook.com/v18.0/oauth/access_token",
124
+ "https://graph.facebook.com/v24.0/oauth/access_token",
125
125
  });
126
126
  },
127
127
  async getUserInfo(token) {
@@ -1,10 +1,12 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
+ import { logger } from "../env";
2
3
  import type { OAuthProvider, ProviderOptions } from "../oauth2";
3
4
  import {
4
5
  createAuthorizationURL,
6
+ getOAuth2Tokens,
5
7
  refreshAccessToken,
6
- validateAuthorizationCode,
7
8
  } from "../oauth2";
9
+ import { createAuthorizationCodeRequest } from "../oauth2/validate-authorization-code";
8
10
 
9
11
  export interface GithubProfile {
10
12
  login: string;
@@ -86,13 +88,33 @@ export const github = (options: GithubOptions) => {
86
88
  });
87
89
  },
88
90
  validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
89
- return validateAuthorizationCode({
91
+ const { body, headers: requestHeaders } = createAuthorizationCodeRequest({
90
92
  code,
91
93
  codeVerifier,
92
94
  redirectURI,
93
95
  options,
94
- tokenEndpoint,
95
96
  });
97
+
98
+ const { data, error } = await betterFetch<
99
+ | { access_token: string; token_type: string; scope: string }
100
+ | { error: string; error_description?: string; error_uri?: string }
101
+ >(tokenEndpoint, {
102
+ method: "POST",
103
+ body: body,
104
+ headers: requestHeaders,
105
+ });
106
+
107
+ if (error) {
108
+ logger.error("GitHub OAuth token exchange failed:", error);
109
+ return null;
110
+ }
111
+
112
+ if ("error" in data) {
113
+ logger.error("GitHub OAuth token exchange failed:", data);
114
+ return null;
115
+ }
116
+
117
+ return getOAuth2Tokens(data);
96
118
  },
97
119
  refreshAccessToken: options.refreshAccessToken
98
120
  ? options.refreshAccessToken
@@ -148,7 +170,7 @@ export const github = (options: GithubOptions) => {
148
170
  return {
149
171
  user: {
150
172
  id: profile.id,
151
- name: profile.name || profile.login,
173
+ name: profile.name || profile.login || "",
152
174
  email: profile.email,
153
175
  image: profile.avatar_url,
154
176
  emailVerified,
@@ -141,7 +141,7 @@ export const gitlab = (options: GitlabOptions) => {
141
141
  return {
142
142
  user: {
143
143
  id: profile.id,
144
- name: profile.name ?? profile.username,
144
+ name: profile.name ?? profile.username ?? "",
145
145
  email: profile.email,
146
146
  image: profile.avatar_url,
147
147
  emailVerified: profile.email_verified ?? false,
@@ -81,7 +81,7 @@ export const google = (options: GoogleOptions) => {
81
81
  const url = await createAuthorizationURL({
82
82
  id: "google",
83
83
  options,
84
- authorizationEndpoint: "https://accounts.google.com/o/oauth2/auth",
84
+ authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
85
85
  scopes: _scopes,
86
86
  state,
87
87
  codeVerifier,
@@ -116,7 +116,7 @@ export const google = (options: GoogleOptions) => {
116
116
  clientKey: options.clientKey,
117
117
  clientSecret: options.clientSecret,
118
118
  },
119
- tokenEndpoint: "https://www.googleapis.com/oauth2/v4/token",
119
+ tokenEndpoint: "https://oauth2.googleapis.com/token",
120
120
  });
121
121
  },
122
122
  async verifyIdToken(token, nonce) {
@@ -130,22 +130,26 @@ export const google = (options: GoogleOptions) => {
130
130
  // Verify JWT integrity
131
131
  // See https://developers.google.com/identity/sign-in/web/backend-auth#verify-the-integrity-of-the-id-token
132
132
 
133
- const { kid, alg: jwtAlg } = decodeProtectedHeader(token);
134
- if (!kid || !jwtAlg) return false;
133
+ try {
134
+ const { kid, alg: jwtAlg } = decodeProtectedHeader(token);
135
+ if (!kid || !jwtAlg) return false;
135
136
 
136
- const publicKey = await getGooglePublicKey(kid);
137
- const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
138
- algorithms: [jwtAlg],
139
- issuer: ["https://accounts.google.com", "accounts.google.com"],
140
- audience: options.clientId,
141
- maxTokenAge: "1h",
142
- });
137
+ const publicKey = await getGooglePublicKey(kid);
138
+ const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
139
+ algorithms: [jwtAlg],
140
+ issuer: ["https://accounts.google.com", "accounts.google.com"],
141
+ audience: options.clientId,
142
+ maxTokenAge: "1h",
143
+ });
144
+
145
+ if (nonce && jwtClaims.nonce !== nonce) {
146
+ return false;
147
+ }
143
148
 
144
- if (nonce && jwtClaims.nonce !== nonce) {
149
+ return true;
150
+ } catch {
145
151
  return false;
146
152
  }
147
-
148
- return true;
149
153
  },
150
154
  async getUserInfo(token) {
151
155
  if (options.getUserInfo) {
@@ -104,7 +104,7 @@ export const huggingface = (options: HuggingFaceOptions) => {
104
104
  return {
105
105
  user: {
106
106
  id: profile.sub,
107
- name: profile.name || profile.preferred_username,
107
+ name: profile.name || profile.preferred_username || "",
108
108
  email: profile.email,
109
109
  image: profile.picture,
110
110
  emailVerified: profile.email_verified ?? false,
@@ -1,4 +1,5 @@
1
1
  import * as z from "zod";
2
+ import type { AwaitableFunction } from "../types";
2
3
  import { apple } from "./apple";
3
4
  import { atlassian } from "./atlassian";
4
5
  import { cognito } from "./cognito";
@@ -21,6 +22,7 @@ import { notion } from "./notion";
21
22
  import { paybin } from "./paybin";
22
23
  import { paypal } from "./paypal";
23
24
  import { polar } from "./polar";
25
+ import { railway } from "./railway";
24
26
  import { reddit } from "./reddit";
25
27
  import { roblox } from "./roblox";
26
28
  import { salesforce } from "./salesforce";
@@ -66,6 +68,7 @@ export const socialProviders = {
66
68
  paybin,
67
69
  paypal,
68
70
  polar,
71
+ railway,
69
72
  vercel,
70
73
  };
71
74
 
@@ -81,11 +84,11 @@ export const SocialProviderListEnum = z
81
84
  export type SocialProvider = z.infer<typeof SocialProviderListEnum>;
82
85
 
83
86
  export type SocialProviders = {
84
- [K in SocialProviderList[number]]?: Parameters<
85
- (typeof socialProviders)[K]
86
- >[0] & {
87
- enabled?: boolean | undefined;
88
- };
87
+ [K in SocialProviderList[number]]?: AwaitableFunction<
88
+ Parameters<(typeof socialProviders)[K]>[0] & {
89
+ enabled?: boolean | undefined;
90
+ }
91
+ >;
89
92
  };
90
93
 
91
94
  export * from "./apple";
@@ -112,6 +115,7 @@ export * from "./notion";
112
115
  export * from "./paybin";
113
116
  export * from "./paypal";
114
117
  export * from "./polar";
118
+ export * from "./railway";
115
119
  export * from "./reddit";
116
120
  export * from "./roblox";
117
121
  export * from "./salesforce";
@@ -161,7 +161,7 @@ export const kakao = (options: KakaoOptions) => {
161
161
  const kakaoProfile = account.profile || {};
162
162
  const user = {
163
163
  id: String(profile.id),
164
- name: kakaoProfile.nickname || account.name || undefined,
164
+ name: kakaoProfile.nickname || account.name || "",
165
165
  email: account.email,
166
166
  image:
167
167
  kakaoProfile.profile_image_url || kakaoProfile.thumbnail_image_url,
@@ -147,7 +147,7 @@ export const line = (options: LineOptions) => {
147
147
  const userMap = await options.mapProfileToUser?.(profile as any);
148
148
  // ID preference order
149
149
  const id = (profile as any).sub || (profile as any).userId;
150
- const name = (profile as any).name || (profile as any).displayName;
150
+ const name = (profile as any).name || (profile as any).displayName || "";
151
151
  const image =
152
152
  (profile as any).picture || (profile as any).pictureUrl || undefined;
153
153
  const email = (profile as any).email;
@@ -1,7 +1,8 @@
1
1
  import { base64 } from "@better-auth/utils/base64";
2
2
  import { betterFetch } from "@better-fetch/fetch";
3
- import { decodeJwt } from "jose";
3
+ import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
4
4
  import { logger } from "../env";
5
+ import { APIError } from "../error";
5
6
  import type { OAuthProvider, ProviderOptions } from "../oauth2";
6
7
  import {
7
8
  createAuthorizationURL,
@@ -174,6 +175,56 @@ export const microsoft = (options: MicrosoftOptions) => {
174
175
  tokenEndpoint,
175
176
  });
176
177
  },
178
+ async verifyIdToken(token, nonce) {
179
+ if (options.disableIdTokenSignIn) {
180
+ return false;
181
+ }
182
+ if (options.verifyIdToken) {
183
+ return options.verifyIdToken(token, nonce);
184
+ }
185
+
186
+ try {
187
+ const { kid, alg: jwtAlg } = decodeProtectedHeader(token);
188
+ if (!kid || !jwtAlg) return false;
189
+
190
+ const publicKey = await getMicrosoftPublicKey(kid, tenant, authority);
191
+ const verifyOptions: {
192
+ algorithms: [string];
193
+ audience: string;
194
+ maxTokenAge: string;
195
+ issuer?: string;
196
+ } = {
197
+ algorithms: [jwtAlg],
198
+ audience: options.clientId,
199
+ maxTokenAge: "1h",
200
+ };
201
+ /**
202
+ * Issuer varies per user's tenant for multi-tenant endpoints, so only validate for specific tenants.
203
+ * @see https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols#endpoints
204
+ */
205
+ if (
206
+ tenant !== "common" &&
207
+ tenant !== "organizations" &&
208
+ tenant !== "consumers"
209
+ ) {
210
+ verifyOptions.issuer = `${authority}/${tenant}/v2.0`;
211
+ }
212
+ const { payload: jwtClaims } = await jwtVerify(
213
+ token,
214
+ publicKey,
215
+ verifyOptions,
216
+ );
217
+
218
+ if (nonce && jwtClaims.nonce !== nonce) {
219
+ return false;
220
+ }
221
+
222
+ return true;
223
+ } catch (error) {
224
+ logger.error("Failed to verify ID token:", error);
225
+ return false;
226
+ }
227
+ },
177
228
  async getUserInfo(token) {
178
229
  if (options.getUserInfo) {
179
230
  return options.getUserInfo(token);
@@ -257,3 +308,35 @@ export const microsoft = (options: MicrosoftOptions) => {
257
308
  options,
258
309
  } satisfies OAuthProvider;
259
310
  };
311
+
312
+ export const getMicrosoftPublicKey = async (
313
+ kid: string,
314
+ tenant: string,
315
+ authority: string,
316
+ ) => {
317
+ const { data } = await betterFetch<{
318
+ keys: Array<{
319
+ kid: string;
320
+ alg: string;
321
+ kty: string;
322
+ use: string;
323
+ n: string;
324
+ e: string;
325
+ x5c?: string[];
326
+ x5t?: string;
327
+ }>;
328
+ }>(`${authority}/${tenant}/discovery/v2.0/keys`);
329
+
330
+ if (!data?.keys) {
331
+ throw new APIError("BAD_REQUEST", {
332
+ message: "Keys not found",
333
+ });
334
+ }
335
+
336
+ const jwk = data.keys.find((key) => key.kid === kid);
337
+ if (!jwk) {
338
+ throw new Error(`JWK with kid ${kid} not found`);
339
+ }
340
+
341
+ return await importJWK(jwk, jwk.alg);
342
+ };