@depup/better-auth__core 1.6.23-depup.2

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 (310) hide show
  1. package/LICENSE.md +20 -0
  2. package/README.md +32 -0
  3. package/changes.json +14 -0
  4. package/dist/api/index.d.mts +281 -0
  5. package/dist/api/index.mjs +92 -0
  6. package/dist/async_hooks/index.d.mts +6 -0
  7. package/dist/async_hooks/index.mjs +20 -0
  8. package/dist/async_hooks/pure.index.d.mts +6 -0
  9. package/dist/async_hooks/pure.index.mjs +33 -0
  10. package/dist/context/endpoint-context.d.mts +18 -0
  11. package/dist/context/endpoint-context.mjs +29 -0
  12. package/dist/context/global.d.mts +6 -0
  13. package/dist/context/global.mjs +36 -0
  14. package/dist/context/index.d.mts +5 -0
  15. package/dist/context/index.mjs +5 -0
  16. package/dist/context/request-state.d.mts +27 -0
  17. package/dist/context/request-state.mjs +47 -0
  18. package/dist/context/transaction.d.mts +27 -0
  19. package/dist/context/transaction.mjs +96 -0
  20. package/dist/db/adapter/factory.d.mts +17 -0
  21. package/dist/db/adapter/factory.mjs +905 -0
  22. package/dist/db/adapter/get-default-field-name.d.mts +18 -0
  23. package/dist/db/adapter/get-default-field-name.mjs +36 -0
  24. package/dist/db/adapter/get-default-model-name.d.mts +12 -0
  25. package/dist/db/adapter/get-default-model-name.mjs +30 -0
  26. package/dist/db/adapter/get-field-attributes.d.mts +26 -0
  27. package/dist/db/adapter/get-field-attributes.mjs +37 -0
  28. package/dist/db/adapter/get-field-name.d.mts +18 -0
  29. package/dist/db/adapter/get-field-name.mjs +31 -0
  30. package/dist/db/adapter/get-id-field.d.mts +36 -0
  31. package/dist/db/adapter/get-id-field.mjs +64 -0
  32. package/dist/db/adapter/get-model-name.d.mts +12 -0
  33. package/dist/db/adapter/get-model-name.mjs +21 -0
  34. package/dist/db/adapter/index.d.mts +615 -0
  35. package/dist/db/adapter/index.mjs +24 -0
  36. package/dist/db/adapter/types.d.mts +105 -0
  37. package/dist/db/adapter/utils.d.mts +7 -0
  38. package/dist/db/adapter/utils.mjs +37 -0
  39. package/dist/db/get-tables.d.mts +6 -0
  40. package/dist/db/get-tables.mjs +266 -0
  41. package/dist/db/index.d.mts +10 -0
  42. package/dist/db/index.mjs +8 -0
  43. package/dist/db/plugin.d.mts +12 -0
  44. package/dist/db/schema/account.d.mts +28 -0
  45. package/dist/db/schema/account.mjs +17 -0
  46. package/dist/db/schema/rate-limit.d.mts +18 -0
  47. package/dist/db/schema/rate-limit.mjs +9 -0
  48. package/dist/db/schema/session.d.mts +23 -0
  49. package/dist/db/schema/session.mjs +12 -0
  50. package/dist/db/schema/shared.d.mts +10 -0
  51. package/dist/db/schema/shared.mjs +9 -0
  52. package/dist/db/schema/user.d.mts +22 -0
  53. package/dist/db/schema/user.mjs +11 -0
  54. package/dist/db/schema/verification.d.mts +21 -0
  55. package/dist/db/schema/verification.mjs +10 -0
  56. package/dist/db/type.d.mts +194 -0
  57. package/dist/env/color-depth.d.mts +4 -0
  58. package/dist/env/color-depth.mjs +86 -0
  59. package/dist/env/env-impl.d.mts +32 -0
  60. package/dist/env/env-impl.mjs +81 -0
  61. package/dist/env/index.d.mts +4 -0
  62. package/dist/env/index.mjs +4 -0
  63. package/dist/env/logger.d.mts +48 -0
  64. package/dist/env/logger.mjs +79 -0
  65. package/dist/error/codes.d.mts +69 -0
  66. package/dist/error/codes.mjs +55 -0
  67. package/dist/error/index.d.mts +26 -0
  68. package/dist/error/index.mjs +27 -0
  69. package/dist/index.d.mts +9 -0
  70. package/dist/index.mjs +1 -0
  71. package/dist/instrumentation/api.mjs +12 -0
  72. package/dist/instrumentation/attributes.d.mts +11 -0
  73. package/dist/instrumentation/attributes.mjs +10 -0
  74. package/dist/instrumentation/index.d.mts +3 -0
  75. package/dist/instrumentation/index.mjs +3 -0
  76. package/dist/instrumentation/noop.mjs +42 -0
  77. package/dist/instrumentation/pure.index.d.mts +7 -0
  78. package/dist/instrumentation/pure.index.mjs +7 -0
  79. package/dist/instrumentation/tracer.d.mts +13 -0
  80. package/dist/instrumentation/tracer.mjs +53 -0
  81. package/dist/oauth2/client-credentials-token.d.mts +56 -0
  82. package/dist/oauth2/client-credentials-token.mjs +64 -0
  83. package/dist/oauth2/create-authorization-url.d.mts +44 -0
  84. package/dist/oauth2/create-authorization-url.mjs +41 -0
  85. package/dist/oauth2/index.d.mts +8 -0
  86. package/dist/oauth2/index.mjs +7 -0
  87. package/dist/oauth2/oauth-provider.d.mts +192 -0
  88. package/dist/oauth2/refresh-access-token.d.mts +54 -0
  89. package/dist/oauth2/refresh-access-token.mjs +73 -0
  90. package/dist/oauth2/reject-redirects.mjs +60 -0
  91. package/dist/oauth2/utils.d.mts +24 -0
  92. package/dist/oauth2/utils.mjs +48 -0
  93. package/dist/oauth2/validate-authorization-code.d.mts +85 -0
  94. package/dist/oauth2/validate-authorization-code.mjs +86 -0
  95. package/dist/oauth2/verify.d.mts +65 -0
  96. package/dist/oauth2/verify.mjs +202 -0
  97. package/dist/social-providers/apple.d.mts +133 -0
  98. package/dist/social-providers/apple.mjs +117 -0
  99. package/dist/social-providers/atlassian.d.mts +70 -0
  100. package/dist/social-providers/atlassian.mjs +80 -0
  101. package/dist/social-providers/cognito.d.mts +85 -0
  102. package/dist/social-providers/cognito.mjs +162 -0
  103. package/dist/social-providers/discord.d.mts +124 -0
  104. package/dist/social-providers/discord.mjs +62 -0
  105. package/dist/social-providers/dropbox.d.mts +69 -0
  106. package/dist/social-providers/dropbox.mjs +72 -0
  107. package/dist/social-providers/facebook.d.mts +79 -0
  108. package/dist/social-providers/facebook.mjs +157 -0
  109. package/dist/social-providers/figma.d.mts +61 -0
  110. package/dist/social-providers/figma.mjs +83 -0
  111. package/dist/social-providers/github.d.mts +102 -0
  112. package/dist/social-providers/github.mjs +92 -0
  113. package/dist/social-providers/gitlab.d.mts +123 -0
  114. package/dist/social-providers/gitlab.mjs +79 -0
  115. package/dist/social-providers/google.d.mts +123 -0
  116. package/dist/social-providers/google.mjs +137 -0
  117. package/dist/social-providers/huggingface.d.mts +83 -0
  118. package/dist/social-providers/huggingface.mjs +73 -0
  119. package/dist/social-providers/index.d.mts +1834 -0
  120. package/dist/social-providers/index.mjs +78 -0
  121. package/dist/social-providers/kakao.d.mts +161 -0
  122. package/dist/social-providers/kakao.mjs +70 -0
  123. package/dist/social-providers/kick.d.mts +73 -0
  124. package/dist/social-providers/kick.mjs +68 -0
  125. package/dist/social-providers/line.d.mts +105 -0
  126. package/dist/social-providers/line.mjs +110 -0
  127. package/dist/social-providers/linear.d.mts +68 -0
  128. package/dist/social-providers/linear.mjs +85 -0
  129. package/dist/social-providers/linkedin.d.mts +67 -0
  130. package/dist/social-providers/linkedin.mjs +73 -0
  131. package/dist/social-providers/microsoft-entra-id.d.mts +174 -0
  132. package/dist/social-providers/microsoft-entra-id.mjs +152 -0
  133. package/dist/social-providers/naver.d.mts +92 -0
  134. package/dist/social-providers/naver.mjs +65 -0
  135. package/dist/social-providers/notion.d.mts +64 -0
  136. package/dist/social-providers/notion.mjs +72 -0
  137. package/dist/social-providers/paybin.d.mts +71 -0
  138. package/dist/social-providers/paybin.mjs +81 -0
  139. package/dist/social-providers/paypal.d.mts +131 -0
  140. package/dist/social-providers/paypal.mjs +188 -0
  141. package/dist/social-providers/polar.d.mts +74 -0
  142. package/dist/social-providers/polar.mjs +71 -0
  143. package/dist/social-providers/railway.d.mts +65 -0
  144. package/dist/social-providers/railway.mjs +74 -0
  145. package/dist/social-providers/reddit.d.mts +62 -0
  146. package/dist/social-providers/reddit.mjs +81 -0
  147. package/dist/social-providers/roblox.d.mts +70 -0
  148. package/dist/social-providers/roblox.mjs +57 -0
  149. package/dist/social-providers/salesforce.d.mts +79 -0
  150. package/dist/social-providers/salesforce.mjs +87 -0
  151. package/dist/social-providers/slack.d.mts +83 -0
  152. package/dist/social-providers/slack.mjs +66 -0
  153. package/dist/social-providers/spotify.d.mts +63 -0
  154. package/dist/social-providers/spotify.mjs +69 -0
  155. package/dist/social-providers/tiktok.d.mts +168 -0
  156. package/dist/social-providers/tiktok.mjs +60 -0
  157. package/dist/social-providers/twitch.d.mts +79 -0
  158. package/dist/social-providers/twitch.mjs +75 -0
  159. package/dist/social-providers/twitter.d.mts +126 -0
  160. package/dist/social-providers/twitter.mjs +85 -0
  161. package/dist/social-providers/vercel.d.mts +62 -0
  162. package/dist/social-providers/vercel.mjs +58 -0
  163. package/dist/social-providers/vk.d.mts +70 -0
  164. package/dist/social-providers/vk.mjs +81 -0
  165. package/dist/social-providers/wechat.d.mts +113 -0
  166. package/dist/social-providers/wechat.mjs +81 -0
  167. package/dist/social-providers/zoom.d.mts +163 -0
  168. package/dist/social-providers/zoom.mjs +69 -0
  169. package/dist/types/context.d.mts +319 -0
  170. package/dist/types/cookie.d.mts +15 -0
  171. package/dist/types/helper.d.mts +10 -0
  172. package/dist/types/index.d.mts +9 -0
  173. package/dist/types/init-options.d.mts +1430 -0
  174. package/dist/types/plugin-client.d.mts +123 -0
  175. package/dist/types/plugin.d.mts +124 -0
  176. package/dist/types/secret.d.mts +11 -0
  177. package/dist/utils/async.d.mts +22 -0
  178. package/dist/utils/async.mjs +32 -0
  179. package/dist/utils/db.d.mts +9 -0
  180. package/dist/utils/db.mjs +15 -0
  181. package/dist/utils/deprecate.d.mts +9 -0
  182. package/dist/utils/deprecate.mjs +16 -0
  183. package/dist/utils/error-codes.d.mts +13 -0
  184. package/dist/utils/error-codes.mjs +10 -0
  185. package/dist/utils/fetch-metadata.d.mts +4 -0
  186. package/dist/utils/fetch-metadata.mjs +6 -0
  187. package/dist/utils/host.d.mts +147 -0
  188. package/dist/utils/host.mjs +295 -0
  189. package/dist/utils/id.d.mts +4 -0
  190. package/dist/utils/id.mjs +7 -0
  191. package/dist/utils/ip.d.mts +77 -0
  192. package/dist/utils/ip.mjs +230 -0
  193. package/dist/utils/is-api-error.d.mts +6 -0
  194. package/dist/utils/is-api-error.mjs +8 -0
  195. package/dist/utils/json.d.mts +4 -0
  196. package/dist/utils/json.mjs +41 -0
  197. package/dist/utils/redirect-uri.d.mts +20 -0
  198. package/dist/utils/redirect-uri.mjs +48 -0
  199. package/dist/utils/string.d.mts +8 -0
  200. package/dist/utils/string.mjs +25 -0
  201. package/dist/utils/url.d.mts +37 -0
  202. package/dist/utils/url.mjs +61 -0
  203. package/package.json +216 -0
  204. package/src/api/index.ts +185 -0
  205. package/src/async_hooks/index.ts +40 -0
  206. package/src/async_hooks/pure.index.ts +46 -0
  207. package/src/context/endpoint-context.ts +50 -0
  208. package/src/context/global.ts +57 -0
  209. package/src/context/index.ts +23 -0
  210. package/src/context/request-state.ts +91 -0
  211. package/src/context/transaction.ts +169 -0
  212. package/src/db/adapter/factory.ts +1740 -0
  213. package/src/db/adapter/get-default-field-name.ts +59 -0
  214. package/src/db/adapter/get-default-model-name.ts +51 -0
  215. package/src/db/adapter/get-field-attributes.ts +62 -0
  216. package/src/db/adapter/get-field-name.ts +43 -0
  217. package/src/db/adapter/get-id-field.ts +150 -0
  218. package/src/db/adapter/get-model-name.ts +36 -0
  219. package/src/db/adapter/index.ts +656 -0
  220. package/src/db/adapter/types.ts +134 -0
  221. package/src/db/adapter/utils.ts +61 -0
  222. package/src/db/get-tables.ts +304 -0
  223. package/src/db/index.ts +43 -0
  224. package/src/db/plugin.ts +11 -0
  225. package/src/db/schema/account.ts +47 -0
  226. package/src/db/schema/rate-limit.ts +36 -0
  227. package/src/db/schema/session.ts +29 -0
  228. package/src/db/schema/shared.ts +7 -0
  229. package/src/db/schema/user.ts +31 -0
  230. package/src/db/schema/verification.ts +28 -0
  231. package/src/db/type.ts +360 -0
  232. package/src/env/color-depth.ts +172 -0
  233. package/src/env/env-impl.ts +123 -0
  234. package/src/env/index.ts +23 -0
  235. package/src/env/logger.ts +145 -0
  236. package/src/error/codes.ts +72 -0
  237. package/src/error/index.ts +44 -0
  238. package/src/index.ts +1 -0
  239. package/src/instrumentation/api.ts +17 -0
  240. package/src/instrumentation/attributes.ts +22 -0
  241. package/src/instrumentation/index.ts +2 -0
  242. package/src/instrumentation/noop.ts +74 -0
  243. package/src/instrumentation/pure.index.ts +31 -0
  244. package/src/instrumentation/tracer.ts +95 -0
  245. package/src/oauth2/client-credentials-token.ts +126 -0
  246. package/src/oauth2/create-authorization-url.ts +89 -0
  247. package/src/oauth2/index.ts +34 -0
  248. package/src/oauth2/oauth-provider.ts +222 -0
  249. package/src/oauth2/refresh-access-token.ts +157 -0
  250. package/src/oauth2/reject-redirects.ts +70 -0
  251. package/src/oauth2/utils.ts +70 -0
  252. package/src/oauth2/validate-authorization-code.ts +190 -0
  253. package/src/oauth2/verify.ts +408 -0
  254. package/src/social-providers/apple.ts +249 -0
  255. package/src/social-providers/atlassian.ts +133 -0
  256. package/src/social-providers/cognito.ts +281 -0
  257. package/src/social-providers/discord.ts +170 -0
  258. package/src/social-providers/dropbox.ts +112 -0
  259. package/src/social-providers/facebook.ts +288 -0
  260. package/src/social-providers/figma.ts +118 -0
  261. package/src/social-providers/github.ts +184 -0
  262. package/src/social-providers/gitlab.ts +155 -0
  263. package/src/social-providers/google.ts +263 -0
  264. package/src/social-providers/huggingface.ts +119 -0
  265. package/src/social-providers/index.ts +132 -0
  266. package/src/social-providers/kakao.ts +179 -0
  267. package/src/social-providers/kick.ts +109 -0
  268. package/src/social-providers/line.ts +169 -0
  269. package/src/social-providers/linear.ts +121 -0
  270. package/src/social-providers/linkedin.ts +110 -0
  271. package/src/social-providers/microsoft-entra-id.ts +391 -0
  272. package/src/social-providers/naver.ts +113 -0
  273. package/src/social-providers/notion.ts +108 -0
  274. package/src/social-providers/paybin.ts +118 -0
  275. package/src/social-providers/paypal.ts +371 -0
  276. package/src/social-providers/polar.ts +111 -0
  277. package/src/social-providers/railway.ts +100 -0
  278. package/src/social-providers/reddit.ts +126 -0
  279. package/src/social-providers/roblox.ts +112 -0
  280. package/src/social-providers/salesforce.ts +159 -0
  281. package/src/social-providers/slack.ts +112 -0
  282. package/src/social-providers/spotify.ts +94 -0
  283. package/src/social-providers/tiktok.ts +211 -0
  284. package/src/social-providers/twitch.ts +112 -0
  285. package/src/social-providers/twitter.ts +199 -0
  286. package/src/social-providers/vercel.ts +87 -0
  287. package/src/social-providers/vk.ts +125 -0
  288. package/src/social-providers/wechat.ts +220 -0
  289. package/src/social-providers/zoom.ts +230 -0
  290. package/src/types/context.ts +460 -0
  291. package/src/types/cookie.ts +10 -0
  292. package/src/types/helper.ts +27 -0
  293. package/src/types/index.ts +40 -0
  294. package/src/types/init-options.ts +1679 -0
  295. package/src/types/plugin-client.ts +143 -0
  296. package/src/types/plugin.ts +163 -0
  297. package/src/types/secret.ts +8 -0
  298. package/src/utils/async.ts +53 -0
  299. package/src/utils/db.ts +20 -0
  300. package/src/utils/deprecate.ts +21 -0
  301. package/src/utils/error-codes.ts +68 -0
  302. package/src/utils/fetch-metadata.ts +3 -0
  303. package/src/utils/host.ts +416 -0
  304. package/src/utils/id.ts +5 -0
  305. package/src/utils/ip.ts +395 -0
  306. package/src/utils/is-api-error.ts +10 -0
  307. package/src/utils/json.ts +56 -0
  308. package/src/utils/redirect-uri.ts +54 -0
  309. package/src/utils/string.ts +40 -0
  310. package/src/utils/url.ts +77 -0
@@ -0,0 +1,190 @@
1
+ import { base64 } from "@better-auth/utils/base64";
2
+ import { createRemoteJWKSet, customFetch, jwtVerify } from "jose";
3
+ import type { AwaitableFunction } from "../types";
4
+ import type { ProviderOptions } from "./index";
5
+ import { getOAuth2Tokens } from "./index";
6
+ import {
7
+ assertResponseNotRedirect,
8
+ fetchRefusingRedirects,
9
+ NO_FOLLOW_REDIRECT,
10
+ } from "./reject-redirects";
11
+
12
+ export async function authorizationCodeRequest({
13
+ code,
14
+ codeVerifier,
15
+ redirectURI,
16
+ options,
17
+ authentication,
18
+ deviceId,
19
+ headers,
20
+ additionalParams = {},
21
+ 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") | undefined;
29
+ headers?: Record<string, string> | undefined;
30
+ additionalParams?: Record<string, string> | undefined;
31
+ resource?: (string | string[]) | undefined;
32
+ }) {
33
+ options = typeof options === "function" ? await options() : options;
34
+ return createAuthorizationCodeRequest({
35
+ code,
36
+ codeVerifier,
37
+ redirectURI,
38
+ options,
39
+ authentication,
40
+ deviceId,
41
+ headers,
42
+ additionalParams,
43
+ resource,
44
+ });
45
+ }
46
+
47
+ /**
48
+ * @deprecated use async'd authorizationCodeRequest instead
49
+ */
50
+ export function createAuthorizationCodeRequest({
51
+ code,
52
+ codeVerifier,
53
+ redirectURI,
54
+ options,
55
+ authentication,
56
+ deviceId,
57
+ headers,
58
+ additionalParams = {},
59
+ resource,
60
+ }: {
61
+ code: string;
62
+ redirectURI: string;
63
+ options: Partial<ProviderOptions>;
64
+ codeVerifier?: string | undefined;
65
+ deviceId?: string | undefined;
66
+ authentication?: ("basic" | "post") | undefined;
67
+ headers?: Record<string, string> | undefined;
68
+ additionalParams?: Record<string, string> | undefined;
69
+ resource?: (string | string[]) | undefined;
70
+ }) {
71
+ const body = new URLSearchParams();
72
+ const requestHeaders: Record<string, any> = {
73
+ "content-type": "application/x-www-form-urlencoded",
74
+ accept: "application/json",
75
+ ...headers,
76
+ };
77
+
78
+ body.set("grant_type", "authorization_code");
79
+ body.set("code", code);
80
+ codeVerifier && body.set("code_verifier", codeVerifier);
81
+ options.clientKey && body.set("client_key", options.clientKey);
82
+ deviceId && body.set("device_id", deviceId);
83
+ body.set("redirect_uri", options.redirectURI || redirectURI);
84
+ if (resource) {
85
+ if (typeof resource === "string") {
86
+ body.append("resource", resource);
87
+ } else {
88
+ for (const _resource of resource) {
89
+ body.append("resource", _resource);
90
+ }
91
+ }
92
+ }
93
+ // Use standard Base64 encoding for HTTP Basic Auth (OAuth2 spec, RFC 7617)
94
+ // Fixes compatibility with providers like Notion, Twitter, etc.
95
+ if (authentication === "basic") {
96
+ const primaryClientId = Array.isArray(options.clientId)
97
+ ? options.clientId[0]
98
+ : options.clientId;
99
+ const encodedCredentials = base64.encode(
100
+ `${primaryClientId}:${options.clientSecret ?? ""}`,
101
+ );
102
+ requestHeaders["authorization"] = `Basic ${encodedCredentials}`;
103
+ } else {
104
+ const primaryClientId = Array.isArray(options.clientId)
105
+ ? options.clientId[0]
106
+ : options.clientId;
107
+ body.set("client_id", primaryClientId);
108
+ if (options.clientSecret) {
109
+ body.set("client_secret", options.clientSecret);
110
+ }
111
+ }
112
+
113
+ for (const [key, value] of Object.entries(additionalParams)) {
114
+ if (!body.has(key)) body.append(key, value);
115
+ }
116
+
117
+ return {
118
+ body,
119
+ headers: requestHeaders,
120
+ };
121
+ }
122
+
123
+ export async function validateAuthorizationCode({
124
+ code,
125
+ codeVerifier,
126
+ redirectURI,
127
+ options,
128
+ tokenEndpoint,
129
+ authentication,
130
+ deviceId,
131
+ headers,
132
+ additionalParams = {},
133
+ resource,
134
+ }: {
135
+ code: string;
136
+ redirectURI: string;
137
+ options: AwaitableFunction<Partial<ProviderOptions>>;
138
+ codeVerifier?: string | undefined;
139
+ deviceId?: string | undefined;
140
+ tokenEndpoint: string;
141
+ authentication?: ("basic" | "post") | undefined;
142
+ headers?: Record<string, string> | undefined;
143
+ additionalParams?: Record<string, string> | undefined;
144
+ resource?: (string | string[]) | undefined;
145
+ }) {
146
+ const { body, headers: requestHeaders } = await authorizationCodeRequest({
147
+ code,
148
+ codeVerifier,
149
+ redirectURI,
150
+ options,
151
+ authentication,
152
+ deviceId,
153
+ headers,
154
+ additionalParams,
155
+ resource,
156
+ });
157
+
158
+ const { data, error } = await fetchRefusingRedirects<object>(tokenEndpoint, {
159
+ method: "POST",
160
+ body: body,
161
+ headers: requestHeaders,
162
+ });
163
+ if (error) {
164
+ throw error;
165
+ }
166
+ const tokens = getOAuth2Tokens(data);
167
+ return tokens;
168
+ }
169
+
170
+ export async function validateToken(
171
+ token: string,
172
+ jwksEndpoint: string,
173
+ options?: {
174
+ audience?: string | string[];
175
+ issuer?: string | string[];
176
+ },
177
+ ) {
178
+ const jwks = createRemoteJWKSet(new URL(jwksEndpoint), {
179
+ [customFetch]: async (url, init) => {
180
+ const response = await fetch(url, { ...init, ...NO_FOLLOW_REDIRECT });
181
+ assertResponseNotRedirect(String(url), response);
182
+ return response;
183
+ },
184
+ });
185
+ const verified = await jwtVerify(token, jwks, {
186
+ audience: options?.audience,
187
+ issuer: options?.issuer,
188
+ });
189
+ return verified;
190
+ }
@@ -0,0 +1,408 @@
1
+ import { APIError } from "better-call";
2
+ import type {
3
+ JSONWebKeySet,
4
+ JWTPayload,
5
+ JWTVerifyOptions,
6
+ JWTVerifyResult,
7
+ ProtectedHeaderParameters,
8
+ } from "jose";
9
+ import {
10
+ createLocalJWKSet,
11
+ decodeProtectedHeader,
12
+ errors as joseErrors,
13
+ jwtVerify,
14
+ UnsecuredJWT,
15
+ } from "jose";
16
+ import { logger } from "../env";
17
+ import { fetchRefusingRedirects } from "./reject-redirects";
18
+
19
+ const joseInfrastructureErrorCodes = new Set([
20
+ joseErrors.JWKSTimeout.code,
21
+ joseErrors.JWKSInvalid.code,
22
+ joseErrors.JWKSMultipleMatchingKeys.code,
23
+ ]);
24
+
25
+ function isJoseInfrastructureError(error: joseErrors.JOSEError) {
26
+ return joseInfrastructureErrorCodes.has(error.code);
27
+ }
28
+
29
+ interface JwksCacheEntry {
30
+ jwks: JSONWebKeySet;
31
+ fetchedAt: number;
32
+ noKidRefetchedAt?: number | undefined;
33
+ }
34
+
35
+ type JwksFetchOptions = {
36
+ /** Jwks url or promise of a Jwks */
37
+ jwksFetch: string | (() => Promise<JSONWebKeySet | undefined>);
38
+ /**
39
+ * Stable object to cache the result of a function `jwksFetch` under,
40
+ * with the same TTL and kid-miss refetch rules as string sources.
41
+ * Without it, a function source is fetched on every verification.
42
+ */
43
+ jwksCacheKey?: object;
44
+ };
45
+
46
+ type ResolvedJwks = {
47
+ jwks: JSONWebKeySet;
48
+ fromCache: boolean;
49
+ kid: string | undefined;
50
+ noKidRefetchedAt?: number | undefined;
51
+ };
52
+
53
+ /**
54
+ * @internal
55
+ */
56
+ export const jwksCache = new Map<string, JwksCacheEntry>();
57
+
58
+ /**
59
+ * Cache for function jwks sources, keyed by a caller-provided stable object.
60
+ * Entries are released with their key, so per-request keys cannot accumulate.
61
+ */
62
+ const functionJwksCache = new WeakMap<object, JwksCacheEntry>();
63
+
64
+ /**
65
+ * How long a cached JWKS is trusted before it is refetched
66
+ *
67
+ * @internal
68
+ */
69
+ const JWKS_CACHE_TTL_MS = 5 * 60 * 1000;
70
+ const JWKS_NO_KID_REFETCH_COOLDOWN_MS = 30 * 1000;
71
+
72
+ /**
73
+ * Returns the cached key set when it is within the TTL. When the token carries
74
+ * `kid`, the cached set must contain that key id; without `kid`, key selection
75
+ * is deferred to JOSE because RFC 7515 makes the header parameter optional.
76
+ */
77
+ function getFreshJwksWithKid(
78
+ cached: JwksCacheEntry | undefined,
79
+ kid: string | undefined,
80
+ ): JSONWebKeySet | undefined {
81
+ if (!cached) return undefined;
82
+ if (Date.now() - cached.fetchedAt >= JWKS_CACHE_TTL_MS) return undefined;
83
+ if (kid && !cached.jwks.keys.some((jwk) => jwk.kid === kid)) {
84
+ return undefined;
85
+ }
86
+ return cached.jwks;
87
+ }
88
+
89
+ function shouldRefetchCachedJwksWithoutKid(
90
+ error: unknown,
91
+ resolved: ResolvedJwks,
92
+ ) {
93
+ const isRetryableNoKidFailure =
94
+ resolved.fromCache &&
95
+ !resolved.kid &&
96
+ (error instanceof joseErrors.JWKSNoMatchingKey ||
97
+ error instanceof joseErrors.JWSSignatureVerificationFailed);
98
+ if (!isRetryableNoKidFailure) return false;
99
+ if (!resolved.noKidRefetchedAt) return true;
100
+ return (
101
+ Date.now() - resolved.noKidRefetchedAt >= JWKS_NO_KID_REFETCH_COOLDOWN_MS
102
+ );
103
+ }
104
+
105
+ async function fetchJwks(
106
+ jwksFetch: JwksFetchOptions["jwksFetch"],
107
+ ): Promise<JSONWebKeySet> {
108
+ const jwks =
109
+ typeof jwksFetch === "string"
110
+ ? await fetchRefusingRedirects<JSONWebKeySet>(jwksFetch, {
111
+ headers: {
112
+ Accept: "application/json",
113
+ },
114
+ }).then(async (res) => {
115
+ if (res.error)
116
+ throw new Error(
117
+ `Jwks failed: ${res.error.message ?? res.error.statusText}`,
118
+ );
119
+ return res.data;
120
+ })
121
+ : await jwksFetch();
122
+ if (!jwks) throw new Error("No jwks found");
123
+ return jwks;
124
+ }
125
+
126
+ export interface VerifyAccessTokenRemote {
127
+ /** Full url of the introspect endpoint. Should end with `/oauth2/introspect` */
128
+ introspectUrl: string;
129
+ /** Client Secret */
130
+ clientId: string;
131
+ /** Client Secret */
132
+ clientSecret: string;
133
+ /**
134
+ * Forces remote verification of a token.
135
+ * This ensures attached session (if applicable)
136
+ * is also still active.
137
+ */
138
+ force?: boolean;
139
+ /**
140
+ * Accept introspection responses that omit the `aud` claim even when a
141
+ * required `audience` is configured in `verifyOptions`.
142
+ *
143
+ * By default verification fails closed: if you configure an `audience` and
144
+ * the introspection response has no `aud` (or a mismatching one), the token
145
+ * is rejected. Some authorization servers legitimately omit `aud` from
146
+ * introspection responses (it is OPTIONAL per RFC 7662 §2.2); only enable
147
+ * this if you trust the issuer to bind the token to this resource through
148
+ * another mechanism, as it skips the audience check in that case.
149
+ *
150
+ * @default false
151
+ */
152
+ allowMissingAudience?: boolean;
153
+ }
154
+
155
+ /**
156
+ * Performs local verification of an access token for your APIs.
157
+ *
158
+ * Can also be configured for remote verification.
159
+ */
160
+ export async function verifyJwsAccessToken(
161
+ token: string,
162
+ opts: JwksFetchOptions & {
163
+ /** Verify options */
164
+ verifyOptions: JWTVerifyOptions &
165
+ Required<Pick<JWTVerifyOptions, "audience" | "issuer">>;
166
+ },
167
+ ) {
168
+ try {
169
+ const resolved = await getJwksForVerification(token, opts);
170
+ let jwt: JWTVerifyResult<JWTPayload>;
171
+ try {
172
+ jwt = await jwtVerify<JWTPayload>(
173
+ token,
174
+ createLocalJWKSet(resolved.jwks),
175
+ opts.verifyOptions,
176
+ );
177
+ } catch (error) {
178
+ if (shouldRefetchCachedJwksWithoutKid(error, resolved)) {
179
+ const refreshed = await getJwksForVerification(token, {
180
+ ...opts,
181
+ forceRefresh: true,
182
+ });
183
+ jwt = await jwtVerify<JWTPayload>(
184
+ token,
185
+ createLocalJWKSet(refreshed.jwks),
186
+ opts.verifyOptions,
187
+ );
188
+ } else {
189
+ throw error;
190
+ }
191
+ }
192
+ // Return the JWT payload in introspection format
193
+ // https://datatracker.ietf.org/doc/html/rfc7662#section-2.2
194
+ if (jwt.payload.azp) {
195
+ jwt.payload.client_id = jwt.payload.azp;
196
+ }
197
+ return jwt.payload;
198
+ } catch (error) {
199
+ if (error instanceof Error) throw error;
200
+ throw new Error(error as unknown as string);
201
+ }
202
+ }
203
+
204
+ export async function getJwks(token: string, opts: JwksFetchOptions) {
205
+ return (await getJwksForVerification(token, opts)).jwks;
206
+ }
207
+
208
+ async function getJwksForVerification(
209
+ token: string,
210
+ opts: JwksFetchOptions & { forceRefresh?: boolean },
211
+ ) {
212
+ // Attempt to decode the token and find a matching kid in jwks
213
+ let jwtHeaders: ProtectedHeaderParameters | undefined;
214
+ try {
215
+ jwtHeaders = decodeProtectedHeader(token);
216
+ } catch (error) {
217
+ if (error instanceof Error) throw error;
218
+ throw new Error(error as unknown as string);
219
+ }
220
+
221
+ const kid = jwtHeaders.kid;
222
+
223
+ // Function sources have no usable identity of their own (callers pass
224
+ // fresh closures per request), so they are cached only under a stable
225
+ // caller-provided key object.
226
+ if (typeof opts.jwksFetch !== "string") {
227
+ const cacheKey = opts.jwksCacheKey;
228
+ if (!cacheKey) {
229
+ const jwks = await opts.jwksFetch();
230
+ if (!jwks) throw new Error("No jwks found");
231
+ return { jwks, fromCache: false, kid };
232
+ }
233
+ const cached = functionJwksCache.get(cacheKey);
234
+ const cachedJwks = opts.forceRefresh
235
+ ? undefined
236
+ : getFreshJwksWithKid(cached, kid);
237
+ if (cachedJwks) {
238
+ return {
239
+ jwks: cachedJwks,
240
+ fromCache: true,
241
+ kid,
242
+ noKidRefetchedAt: cached?.noKidRefetchedAt,
243
+ };
244
+ }
245
+ const jwks = await opts.jwksFetch();
246
+ if (!jwks) throw new Error("No jwks found");
247
+ const fetchedAt = Date.now();
248
+ functionJwksCache.set(cacheKey, {
249
+ jwks,
250
+ fetchedAt,
251
+ ...(opts.forceRefresh && !kid ? { noKidRefetchedAt: fetchedAt } : {}),
252
+ });
253
+ return { jwks, fromCache: false, kid };
254
+ }
255
+
256
+ // The cache is scoped to `cacheKey`, so a token is only ever matched
257
+ // against the key set published by its own source.
258
+ const cacheKey = opts.jwksFetch;
259
+ const cached = jwksCache.get(cacheKey);
260
+ const cachedJwks = opts.forceRefresh
261
+ ? undefined
262
+ : getFreshJwksWithKid(cached, kid);
263
+ if (!cachedJwks) {
264
+ const jwks = await fetchJwks(opts.jwksFetch);
265
+ const fetchedAt = Date.now();
266
+ jwksCache.set(cacheKey, {
267
+ jwks,
268
+ fetchedAt,
269
+ ...(opts.forceRefresh && !kid ? { noKidRefetchedAt: fetchedAt } : {}),
270
+ });
271
+ return { jwks, fromCache: false, kid };
272
+ }
273
+
274
+ return {
275
+ jwks: cachedJwks,
276
+ fromCache: true,
277
+ kid,
278
+ noKidRefetchedAt: cached?.noKidRefetchedAt,
279
+ };
280
+ }
281
+
282
+ /**
283
+ * Performs local verification of an access token for your API.
284
+ *
285
+ * Can also be configured for remote verification.
286
+ */
287
+ export async function verifyAccessToken(
288
+ token: string,
289
+ opts: {
290
+ /** Verify options */
291
+ verifyOptions: JWTVerifyOptions &
292
+ Required<Pick<JWTVerifyOptions, "audience" | "issuer">>;
293
+ /** Scopes to additionally verify. Token must include all but not exact. */
294
+ scopes?: string[];
295
+ /** Required to verify access token locally */
296
+ jwksUrl?: string;
297
+ /** If provided, can verify a token remotely */
298
+ remoteVerify?: VerifyAccessTokenRemote;
299
+ },
300
+ ) {
301
+ let payload: JWTPayload | undefined;
302
+ // Locally verify
303
+ if (opts.jwksUrl && !opts?.remoteVerify?.force) {
304
+ try {
305
+ payload = await verifyJwsAccessToken(token, {
306
+ jwksFetch: opts.jwksUrl,
307
+ verifyOptions: opts.verifyOptions,
308
+ });
309
+ } catch (error) {
310
+ if (error instanceof Error) {
311
+ if (error.name === "TypeError" || error.name === "JWSInvalid") {
312
+ // likely an opaque token (continue)
313
+ } else if (error instanceof joseErrors.JWTExpired) {
314
+ throw new APIError("UNAUTHORIZED", {
315
+ message: "token expired",
316
+ });
317
+ } else if (error instanceof joseErrors.JOSEError) {
318
+ if (isJoseInfrastructureError(error)) {
319
+ throw error;
320
+ }
321
+ throw new APIError("UNAUTHORIZED", {
322
+ message: "invalid access token",
323
+ });
324
+ } else {
325
+ throw error;
326
+ }
327
+ } else {
328
+ throw new Error(error as unknown as string);
329
+ }
330
+ }
331
+ }
332
+
333
+ // Remote verify
334
+ if (opts?.remoteVerify) {
335
+ const { data: introspect, error: introspectError } =
336
+ await fetchRefusingRedirects<
337
+ JWTPayload & {
338
+ active: boolean;
339
+ }
340
+ >(opts.remoteVerify.introspectUrl, {
341
+ method: "POST",
342
+ headers: {
343
+ Accept: "application/json",
344
+ "Content-Type": "application/x-www-form-urlencoded",
345
+ },
346
+ body: new URLSearchParams({
347
+ client_id: opts.remoteVerify.clientId,
348
+ client_secret: opts.remoteVerify.clientSecret,
349
+ token,
350
+ token_type_hint: "access_token",
351
+ }).toString(),
352
+ });
353
+ if (introspectError)
354
+ logger.error(
355
+ `Introspection failed: ${introspectError.message ?? introspectError.statusText}`,
356
+ );
357
+ if (!introspect)
358
+ throw new APIError("INTERNAL_SERVER_ERROR", {
359
+ message: "introspection failed",
360
+ });
361
+ if (!introspect.active)
362
+ throw new APIError("UNAUTHORIZED", {
363
+ message: "token inactive",
364
+ });
365
+ // Verifies payload using verify options (token valid through introspect).
366
+ // Audience is enforced by default: when `verifyOptions.audience` is set
367
+ // but the introspection response omits `aud` (or it mismatches),
368
+ // `UnsecuredJWT.decode` throws and the token is rejected. Otherwise a
369
+ // token issued for a different resource/client on the same issuer would
370
+ // also pass. Only drop the audience check when the caller has explicitly
371
+ // opted in via `remoteVerify.allowMissingAudience`.
372
+ try {
373
+ const unsecuredJwt = new UnsecuredJWT(introspect).encode();
374
+ const { audience: _audience, ...verifyOptionsNoAudience } =
375
+ opts.verifyOptions;
376
+ const skipAudience =
377
+ !introspect.aud && opts.remoteVerify.allowMissingAudience === true;
378
+ const verify = UnsecuredJWT.decode(
379
+ unsecuredJwt,
380
+ skipAudience ? verifyOptionsNoAudience : opts.verifyOptions,
381
+ );
382
+ payload = verify.payload;
383
+ } catch (error) {
384
+ throw new Error(error as unknown as string);
385
+ }
386
+ }
387
+
388
+ if (!payload)
389
+ throw new APIError("UNAUTHORIZED", {
390
+ message: `no token payload`,
391
+ });
392
+
393
+ // Check scopes if provided
394
+ if (opts.scopes) {
395
+ const validScopes = new Set(
396
+ (payload.scope as string | undefined)?.split(" "),
397
+ );
398
+ for (const sc of opts.scopes) {
399
+ if (!validScopes.has(sc)) {
400
+ throw new APIError("FORBIDDEN", {
401
+ message: `invalid scope ${sc}`,
402
+ });
403
+ }
404
+ }
405
+ }
406
+
407
+ return payload;
408
+ }