@draftlab/auth 0.15.1 → 0.16.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 (272) hide show
  1. package/dist/esm/allow.js +26 -0
  2. package/dist/esm/client.js +254 -0
  3. package/dist/esm/core.js +597 -0
  4. package/dist/esm/css.d.js +0 -0
  5. package/dist/esm/error.js +88 -0
  6. package/dist/esm/index.js +5 -0
  7. package/dist/esm/keys.js +126 -0
  8. package/dist/esm/mutex.js +53 -0
  9. package/dist/esm/pkce.js +87 -0
  10. package/dist/esm/provider/apple.js +15 -0
  11. package/dist/esm/provider/code.js +62 -0
  12. package/dist/esm/provider/discord.js +15 -0
  13. package/dist/esm/provider/facebook.js +15 -0
  14. package/dist/esm/provider/github.js +15 -0
  15. package/dist/esm/provider/gitlab.js +15 -0
  16. package/dist/esm/provider/google.js +16 -0
  17. package/dist/esm/provider/linkedin.js +15 -0
  18. package/dist/esm/provider/magiclink.js +83 -0
  19. package/dist/esm/provider/microsoft.js +15 -0
  20. package/dist/esm/provider/oauth2.js +130 -0
  21. package/dist/esm/provider/password.js +331 -0
  22. package/dist/esm/provider/provider.js +18 -0
  23. package/dist/esm/provider/reddit.js +15 -0
  24. package/dist/esm/provider/slack.js +15 -0
  25. package/dist/esm/provider/spotify.js +15 -0
  26. package/dist/esm/provider/twitch.js +15 -0
  27. package/dist/esm/provider/vercel.js +17 -0
  28. package/dist/esm/random.js +40 -0
  29. package/dist/esm/revocation.js +27 -0
  30. package/dist/esm/storage/memory.js +110 -0
  31. package/dist/esm/storage/storage.js +56 -0
  32. package/dist/esm/storage/turso.js +93 -0
  33. package/dist/esm/storage/unstorage.js +78 -0
  34. package/dist/esm/subject.js +7 -0
  35. package/dist/esm/themes/theme.js +115 -0
  36. package/dist/esm/toolkit/client.js +119 -0
  37. package/dist/esm/toolkit/index.js +25 -0
  38. package/dist/esm/toolkit/providers/facebook.js +11 -0
  39. package/dist/esm/toolkit/providers/github.js +11 -0
  40. package/dist/esm/toolkit/providers/google.js +11 -0
  41. package/dist/esm/toolkit/providers/strategy.js +0 -0
  42. package/dist/esm/toolkit/storage.js +81 -0
  43. package/dist/esm/toolkit/utils.js +18 -0
  44. package/dist/esm/types.js +0 -0
  45. package/dist/esm/ui/base.js +478 -0
  46. package/dist/esm/ui/code.js +186 -0
  47. package/dist/esm/ui/form.js +46 -0
  48. package/dist/esm/ui/icon.js +242 -0
  49. package/dist/esm/ui/magiclink.js +158 -0
  50. package/dist/esm/ui/password.js +435 -0
  51. package/dist/esm/ui/select.js +102 -0
  52. package/dist/esm/util.js +59 -0
  53. package/dist/{allow.d.mts → types/allow.d.ts} +9 -11
  54. package/dist/types/allow.d.ts.map +1 -0
  55. package/dist/types/client.d.ts +462 -0
  56. package/dist/types/client.d.ts.map +1 -0
  57. package/dist/types/core.d.ts +113 -0
  58. package/dist/types/core.d.ts.map +1 -0
  59. package/dist/{error.d.mts → types/error.d.ts} +95 -97
  60. package/dist/types/error.d.ts.map +1 -0
  61. package/dist/types/index.d.ts +2 -0
  62. package/dist/types/index.d.ts.map +1 -0
  63. package/dist/{keys.d.mts → types/keys.d.ts} +20 -23
  64. package/dist/types/keys.d.ts.map +1 -0
  65. package/dist/types/mutex.d.ts +42 -0
  66. package/dist/types/mutex.d.ts.map +1 -0
  67. package/dist/{pkce.d.mts → types/pkce.d.ts} +10 -11
  68. package/dist/types/pkce.d.ts.map +1 -0
  69. package/dist/types/provider/apple.d.ts +197 -0
  70. package/dist/types/provider/apple.d.ts.map +1 -0
  71. package/dist/types/provider/code.d.ts +288 -0
  72. package/dist/types/provider/code.d.ts.map +1 -0
  73. package/dist/types/provider/discord.d.ts +206 -0
  74. package/dist/types/provider/discord.d.ts.map +1 -0
  75. package/dist/types/provider/facebook.d.ts +200 -0
  76. package/dist/types/provider/facebook.d.ts.map +1 -0
  77. package/dist/types/provider/github.d.ts +220 -0
  78. package/dist/types/provider/github.d.ts.map +1 -0
  79. package/dist/types/provider/gitlab.d.ts +180 -0
  80. package/dist/types/provider/gitlab.d.ts.map +1 -0
  81. package/dist/types/provider/google.d.ts +158 -0
  82. package/dist/types/provider/google.d.ts.map +1 -0
  83. package/dist/types/provider/linkedin.d.ts +190 -0
  84. package/dist/types/provider/linkedin.d.ts.map +1 -0
  85. package/dist/types/provider/magiclink.d.ts +141 -0
  86. package/dist/types/provider/magiclink.d.ts.map +1 -0
  87. package/dist/types/provider/microsoft.d.ts +247 -0
  88. package/dist/types/provider/microsoft.d.ts.map +1 -0
  89. package/dist/types/provider/oauth2.d.ts +229 -0
  90. package/dist/types/provider/oauth2.d.ts.map +1 -0
  91. package/dist/types/provider/password.d.ts +408 -0
  92. package/dist/types/provider/password.d.ts.map +1 -0
  93. package/dist/types/provider/provider.d.ts +226 -0
  94. package/dist/types/provider/provider.d.ts.map +1 -0
  95. package/dist/types/provider/reddit.d.ts +159 -0
  96. package/dist/types/provider/reddit.d.ts.map +1 -0
  97. package/dist/types/provider/slack.d.ts +171 -0
  98. package/dist/types/provider/slack.d.ts.map +1 -0
  99. package/dist/types/provider/spotify.d.ts +168 -0
  100. package/dist/types/provider/spotify.d.ts.map +1 -0
  101. package/dist/types/provider/twitch.d.ts +163 -0
  102. package/dist/types/provider/twitch.d.ts.map +1 -0
  103. package/dist/types/provider/vercel.d.ts +294 -0
  104. package/dist/types/provider/vercel.d.ts.map +1 -0
  105. package/dist/{random.d.mts → types/random.d.ts} +4 -6
  106. package/dist/types/random.d.ts.map +1 -0
  107. package/dist/types/revocation.d.ts +76 -0
  108. package/dist/types/revocation.d.ts.map +1 -0
  109. package/dist/{storage/memory.d.mts → types/storage/memory.d.ts} +17 -20
  110. package/dist/types/storage/memory.d.ts.map +1 -0
  111. package/dist/types/storage/storage.d.ts +177 -0
  112. package/dist/types/storage/storage.d.ts.map +1 -0
  113. package/dist/{storage/turso.d.mts → types/storage/turso.d.ts} +4 -7
  114. package/dist/types/storage/turso.d.ts.map +1 -0
  115. package/dist/{storage/unstorage.d.mts → types/storage/unstorage.d.ts} +12 -10
  116. package/dist/types/storage/unstorage.d.ts.map +1 -0
  117. package/dist/types/subject.d.ts +115 -0
  118. package/dist/types/subject.d.ts.map +1 -0
  119. package/dist/types/themes/theme.d.ts +207 -0
  120. package/dist/types/themes/theme.d.ts.map +1 -0
  121. package/dist/types/toolkit/client.d.ts +235 -0
  122. package/dist/types/toolkit/client.d.ts.map +1 -0
  123. package/dist/types/toolkit/index.d.ts +45 -0
  124. package/dist/types/toolkit/index.d.ts.map +1 -0
  125. package/dist/types/toolkit/providers/facebook.d.ts +8 -0
  126. package/dist/types/toolkit/providers/facebook.d.ts.map +1 -0
  127. package/dist/types/toolkit/providers/github.d.ts +8 -0
  128. package/dist/types/toolkit/providers/github.d.ts.map +1 -0
  129. package/dist/types/toolkit/providers/google.d.ts +8 -0
  130. package/dist/types/toolkit/providers/google.d.ts.map +1 -0
  131. package/dist/types/toolkit/providers/strategy.d.ts +38 -0
  132. package/dist/types/toolkit/providers/strategy.d.ts.map +1 -0
  133. package/dist/{toolkit/storage.d.mts → types/toolkit/storage.d.ts} +37 -39
  134. package/dist/types/toolkit/storage.d.ts.map +1 -0
  135. package/dist/{toolkit/utils.d.mts → types/toolkit/utils.d.ts} +2 -4
  136. package/dist/types/toolkit/utils.d.ts.map +1 -0
  137. package/dist/types/types.d.ts +92 -0
  138. package/dist/types/types.d.ts.map +1 -0
  139. package/dist/types/ui/base.d.ts +18 -0
  140. package/dist/types/ui/base.d.ts.map +1 -0
  141. package/dist/types/ui/code.d.ts +43 -0
  142. package/dist/types/ui/code.d.ts.map +1 -0
  143. package/dist/types/ui/form.d.ts +24 -0
  144. package/dist/types/ui/form.d.ts.map +1 -0
  145. package/dist/types/ui/icon.d.ts +60 -0
  146. package/dist/types/ui/icon.d.ts.map +1 -0
  147. package/dist/types/ui/magiclink.d.ts +41 -0
  148. package/dist/types/ui/magiclink.d.ts.map +1 -0
  149. package/dist/types/ui/password.d.ts +43 -0
  150. package/dist/types/ui/password.d.ts.map +1 -0
  151. package/dist/types/ui/select.d.ts +33 -0
  152. package/dist/types/ui/select.d.ts.map +1 -0
  153. package/dist/{util.d.mts → types/util.d.ts} +11 -12
  154. package/dist/types/util.d.ts.map +1 -0
  155. package/package.json +10 -16
  156. package/dist/adapters/node.d.mts +0 -17
  157. package/dist/adapters/node.mjs +0 -69
  158. package/dist/allow.mjs +0 -63
  159. package/dist/client.d.mts +0 -462
  160. package/dist/client.mjs +0 -284
  161. package/dist/core.d.mts +0 -109
  162. package/dist/core.mjs +0 -595
  163. package/dist/error.mjs +0 -237
  164. package/dist/index.d.mts +0 -2
  165. package/dist/index.mjs +0 -3
  166. package/dist/keys.mjs +0 -146
  167. package/dist/mutex.d.mts +0 -44
  168. package/dist/mutex.mjs +0 -110
  169. package/dist/pkce.mjs +0 -157
  170. package/dist/provider/apple.d.mts +0 -110
  171. package/dist/provider/apple.mjs +0 -164
  172. package/dist/provider/code.d.mts +0 -218
  173. package/dist/provider/code.mjs +0 -246
  174. package/dist/provider/discord.d.mts +0 -145
  175. package/dist/provider/discord.mjs +0 -156
  176. package/dist/provider/facebook.d.mts +0 -141
  177. package/dist/provider/facebook.mjs +0 -150
  178. package/dist/provider/github.d.mts +0 -139
  179. package/dist/provider/github.mjs +0 -169
  180. package/dist/provider/gitlab.d.mts +0 -105
  181. package/dist/provider/gitlab.mjs +0 -147
  182. package/dist/provider/google.d.mts +0 -111
  183. package/dist/provider/google.mjs +0 -109
  184. package/dist/provider/linkedin.d.mts +0 -131
  185. package/dist/provider/linkedin.mjs +0 -142
  186. package/dist/provider/magiclink.d.mts +0 -79
  187. package/dist/provider/magiclink.mjs +0 -143
  188. package/dist/provider/microsoft.d.mts +0 -177
  189. package/dist/provider/microsoft.mjs +0 -177
  190. package/dist/provider/oauth2.d.mts +0 -175
  191. package/dist/provider/oauth2.mjs +0 -222
  192. package/dist/provider/passkey.d.mts +0 -103
  193. package/dist/provider/passkey.mjs +0 -320
  194. package/dist/provider/password.d.mts +0 -384
  195. package/dist/provider/password.mjs +0 -363
  196. package/dist/provider/provider.d.mts +0 -225
  197. package/dist/provider/provider.mjs +0 -44
  198. package/dist/provider/reddit.d.mts +0 -106
  199. package/dist/provider/reddit.mjs +0 -127
  200. package/dist/provider/slack.d.mts +0 -113
  201. package/dist/provider/slack.mjs +0 -138
  202. package/dist/provider/spotify.d.mts +0 -112
  203. package/dist/provider/spotify.mjs +0 -135
  204. package/dist/provider/totp.d.mts +0 -111
  205. package/dist/provider/totp.mjs +0 -191
  206. package/dist/provider/twitch.d.mts +0 -107
  207. package/dist/provider/twitch.mjs +0 -131
  208. package/dist/provider/vercel.d.mts +0 -176
  209. package/dist/provider/vercel.mjs +0 -230
  210. package/dist/random.mjs +0 -86
  211. package/dist/revocation.d.mts +0 -54
  212. package/dist/revocation.mjs +0 -63
  213. package/dist/router/context.d.mts +0 -21
  214. package/dist/router/context.mjs +0 -193
  215. package/dist/router/cookies.d.mts +0 -8
  216. package/dist/router/cookies.mjs +0 -13
  217. package/dist/router/index.d.mts +0 -21
  218. package/dist/router/index.mjs +0 -107
  219. package/dist/router/matcher.d.mts +0 -15
  220. package/dist/router/matcher.mjs +0 -76
  221. package/dist/router/middleware/cors.d.mts +0 -15
  222. package/dist/router/middleware/cors.mjs +0 -114
  223. package/dist/router/safe-request.d.mts +0 -52
  224. package/dist/router/safe-request.mjs +0 -160
  225. package/dist/router/types.d.mts +0 -67
  226. package/dist/router/types.mjs +0 -1
  227. package/dist/router/variables.d.mts +0 -12
  228. package/dist/router/variables.mjs +0 -20
  229. package/dist/storage/memory.mjs +0 -125
  230. package/dist/storage/storage.d.mts +0 -179
  231. package/dist/storage/storage.mjs +0 -104
  232. package/dist/storage/turso.mjs +0 -117
  233. package/dist/storage/unstorage.mjs +0 -103
  234. package/dist/subject.d.mts +0 -61
  235. package/dist/subject.mjs +0 -36
  236. package/dist/themes/theme.d.mts +0 -209
  237. package/dist/themes/theme.mjs +0 -120
  238. package/dist/toolkit/client.d.mts +0 -168
  239. package/dist/toolkit/client.mjs +0 -209
  240. package/dist/toolkit/index.d.mts +0 -9
  241. package/dist/toolkit/index.mjs +0 -9
  242. package/dist/toolkit/providers/facebook.d.mts +0 -11
  243. package/dist/toolkit/providers/facebook.mjs +0 -16
  244. package/dist/toolkit/providers/github.d.mts +0 -11
  245. package/dist/toolkit/providers/github.mjs +0 -16
  246. package/dist/toolkit/providers/google.d.mts +0 -11
  247. package/dist/toolkit/providers/google.mjs +0 -20
  248. package/dist/toolkit/providers/strategy.d.mts +0 -40
  249. package/dist/toolkit/providers/strategy.mjs +0 -1
  250. package/dist/toolkit/storage.mjs +0 -157
  251. package/dist/toolkit/utils.mjs +0 -30
  252. package/dist/types.d.mts +0 -94
  253. package/dist/types.mjs +0 -1
  254. package/dist/ui/base.d.mts +0 -29
  255. package/dist/ui/base.mjs +0 -407
  256. package/dist/ui/code.d.mts +0 -42
  257. package/dist/ui/code.mjs +0 -173
  258. package/dist/ui/form.d.mts +0 -31
  259. package/dist/ui/form.mjs +0 -49
  260. package/dist/ui/icon.d.mts +0 -57
  261. package/dist/ui/icon.mjs +0 -247
  262. package/dist/ui/magiclink.d.mts +0 -40
  263. package/dist/ui/magiclink.mjs +0 -152
  264. package/dist/ui/passkey.d.mts +0 -26
  265. package/dist/ui/passkey.mjs +0 -323
  266. package/dist/ui/password.d.mts +0 -41
  267. package/dist/ui/password.mjs +0 -402
  268. package/dist/ui/select.d.mts +0 -33
  269. package/dist/ui/select.mjs +0 -98
  270. package/dist/ui/totp.d.mts +0 -33
  271. package/dist/ui/totp.mjs +0 -270
  272. package/dist/util.mjs +0 -128
@@ -0,0 +1,597 @@
1
+ // src/core.ts
2
+ import { Hono } from "hono";
3
+ import { deleteCookie, getCookie, setCookie } from "hono/cookie";
4
+ import { cors } from "hono/cors";
5
+ import { CompactEncrypt, compactDecrypt, SignJWT } from "jose";
6
+ import { defaultAllowCheck } from "./allow";
7
+ import {
8
+ MissingParameterError,
9
+ OauthError,
10
+ UnauthorizedClientError,
11
+ UnknownStateError
12
+ } from "./error";
13
+ import { encryptionKeys, signingKeys } from "./keys";
14
+ import { validatePKCE } from "./pkce";
15
+ import { generateSecureToken } from "./random";
16
+ import { Revocation } from "./revocation";
17
+ import { Storage } from "./storage/storage";
18
+ import { setTheme } from "./themes/theme";
19
+ import { Select } from "./ui/select";
20
+ import { getRelativeUrl, lazy } from "./util";
21
+ var normalizeTimingAsync = async (fn, minTimeMs = 100) => {
22
+ const startTime = performance.now();
23
+ const result = await fn();
24
+ const elapsed = performance.now() - startTime;
25
+ const remainingTime = Math.max(0, minTimeMs - elapsed);
26
+ if (remainingTime > 0) {
27
+ const jitterBuffer = new Uint32Array(1);
28
+ crypto.getRandomValues(jitterBuffer);
29
+ const jitter = (jitterBuffer[0] ?? 0) / 4294967295 * 20;
30
+ const totalDelay = remainingTime + jitter;
31
+ await new Promise((resolve) => setTimeout(resolve, totalDelay));
32
+ }
33
+ return result;
34
+ };
35
+ var isHttpsRequest = (c) => {
36
+ return c.req.header("x-forwarded-proto") === "https" || c.req.header("x-forwarded-ssl") === "on" || c.req.url.startsWith("https://");
37
+ };
38
+ var issuer = (input) => {
39
+ const error = input.error ?? ((err) => {
40
+ return new Response(err.message, {
41
+ status: 400,
42
+ headers: { "Content-Type": "text/plain" }
43
+ });
44
+ });
45
+ const ttlAccess = input.ttl?.access ?? 60 * 60 * 24 * 30;
46
+ const ttlRefresh = input.ttl?.refresh ?? 60 * 60 * 24 * 365;
47
+ const ttlRefreshReuse = input.ttl?.reuse ?? 60;
48
+ const ttlRefreshRetention = input.ttl?.retention ?? 0;
49
+ if (input.theme) {
50
+ setTheme(input.theme);
51
+ }
52
+ const storage = input.storage;
53
+ const select = lazy(() => input.select ?? Select());
54
+ const allSigning = lazy(() => signingKeys(storage));
55
+ const allEncryption = lazy(() => encryptionKeys(storage));
56
+ const allow = lazy(() => input.allow ?? defaultAllowCheck);
57
+ const signingKey = lazy(() => allSigning().then((all) => all[0]));
58
+ const encryptionKey = lazy(() => allEncryption().then((all) => all[0]));
59
+ const cookiePath = input.basePath || "/";
60
+ const issuerUrl = (c) => {
61
+ const baseUrl = new URL(getRelativeUrl(c, "/"));
62
+ if (input.basePath) {
63
+ const normalizedBasePath = input.basePath.startsWith("/") ? input.basePath : `/${input.basePath}`;
64
+ baseUrl.pathname = normalizedBasePath.replace(/\/$/, "");
65
+ return baseUrl.href;
66
+ }
67
+ return baseUrl.origin;
68
+ };
69
+ const encrypt = async (value) => {
70
+ const key = await encryptionKey();
71
+ if (!key) {
72
+ throw new Error("Encryption key not available");
73
+ }
74
+ return await new CompactEncrypt(new TextEncoder().encode(JSON.stringify(value))).setProtectedHeader({ alg: "RSA-OAEP-512", enc: "A256GCM" }).encrypt(key.public);
75
+ };
76
+ const decrypt = async (value) => {
77
+ const key = await encryptionKey();
78
+ if (!key) {
79
+ throw new Error("Encryption key not available");
80
+ }
81
+ return JSON.parse(new TextDecoder().decode(await compactDecrypt(value, key.private).then((result) => result.plaintext)));
82
+ };
83
+ const resolveSubject = async (type, properties) => {
84
+ const jsonString = JSON.stringify(properties);
85
+ const encoder = new TextEncoder;
86
+ const data = encoder.encode(jsonString);
87
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
88
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
89
+ const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
90
+ return `${type}:${hashHex.slice(0, 16)}`;
91
+ };
92
+ const generateTokens = async (c, value, opts) => {
93
+ const refreshToken = value.nextToken ?? generateSecureToken();
94
+ if (opts?.generateRefreshToken ?? true) {
95
+ const refreshPayload = {
96
+ type: value.type,
97
+ properties: value.properties,
98
+ clientID: value.clientID,
99
+ subject: value.subject,
100
+ ttl: value.ttl,
101
+ nextToken: generateSecureToken(),
102
+ timeUsed: value.timeUsed
103
+ };
104
+ const refreshKey = ["oauth:refresh", value.subject, refreshToken];
105
+ await Storage.set(storage, refreshKey, refreshPayload, value.ttl.refresh);
106
+ }
107
+ const signingKeyData = await signingKey();
108
+ if (!signingKeyData) {
109
+ throw new Error("Signing key not available");
110
+ }
111
+ const now = Math.floor(Date.now() / 1000);
112
+ if (!value.clientID.trim()) {
113
+ throw new Error("Invalid audience: client ID cannot be empty");
114
+ }
115
+ const accessPayload = {
116
+ type: value.type,
117
+ properties: value.properties,
118
+ sub: value.subject,
119
+ aud: value.clientID,
120
+ iss: issuerUrl(c),
121
+ exp: now + value.ttl.access,
122
+ iat: now,
123
+ mode: "access"
124
+ };
125
+ const access = await new SignJWT(accessPayload).setExpirationTime(Math.floor(now + value.ttl.access)).setProtectedHeader({
126
+ alg: signingKeyData.alg,
127
+ kid: signingKeyData.id,
128
+ typ: "JWT"
129
+ }).sign(signingKeyData.private);
130
+ return {
131
+ access,
132
+ refresh: [value.subject, refreshToken].join(":"),
133
+ expiresIn: Math.floor(now + value.ttl.access - Date.now() / 1000)
134
+ };
135
+ };
136
+ const getAuthorization = async (c) => {
137
+ const match = await auth.get(c, "authorization") || c.get("authorization");
138
+ if (!match) {
139
+ throw new UnknownStateError;
140
+ }
141
+ return match;
142
+ };
143
+ const auth = {
144
+ async success(c, properties, successOpts) {
145
+ const authorization = await getAuthorization(c);
146
+ const currentProvider = c.get("provider") || "unknown";
147
+ if (!authorization.client_id) {
148
+ throw new Error("client_id is required");
149
+ }
150
+ return await input.success({
151
+ async subject(type, properties2, subjectOpts) {
152
+ const subject = subjectOpts?.subject ?? await resolveSubject(type, properties2);
153
+ await successOpts?.invalidate?.(await resolveSubject(type, properties2));
154
+ if (authorization.response_type === "token") {
155
+ if (!authorization.redirect_uri) {
156
+ throw new Error("redirect_uri is required");
157
+ }
158
+ const location2 = new URL(authorization.redirect_uri);
159
+ if (!authorization.client_id) {
160
+ throw new Error("client_id is required");
161
+ }
162
+ const tokens = await generateTokens(c, {
163
+ type,
164
+ subject,
165
+ properties: properties2,
166
+ clientID: authorization.client_id,
167
+ ttl: {
168
+ access: subjectOpts?.ttl?.access ?? ttlAccess,
169
+ refresh: subjectOpts?.ttl?.refresh ?? ttlRefresh
170
+ }
171
+ });
172
+ location2.hash = new URLSearchParams({
173
+ access_token: tokens.access,
174
+ token_type: "Bearer",
175
+ expires_in: tokens.expiresIn.toString(),
176
+ ...authorization.state && { state: authorization.state }
177
+ }).toString();
178
+ await auth.unset(c, "authorization");
179
+ return c.redirect(location2.toString(), 302);
180
+ }
181
+ if (!authorization.redirect_uri) {
182
+ throw new Error("redirect_uri is required");
183
+ }
184
+ if (!authorization.client_id) {
185
+ throw new Error("client_id is required");
186
+ }
187
+ const code = generateSecureToken();
188
+ const codePayload = {
189
+ type,
190
+ properties: properties2,
191
+ subject,
192
+ redirectURI: authorization.redirect_uri,
193
+ clientID: authorization.client_id,
194
+ scopes: authorization.scopes,
195
+ pkce: authorization.pkce,
196
+ ttl: {
197
+ access: subjectOpts?.ttl?.access ?? ttlAccess,
198
+ refresh: subjectOpts?.ttl?.refresh ?? ttlRefresh
199
+ }
200
+ };
201
+ await Storage.set(storage, ["oauth:code", code], codePayload, 60);
202
+ if (!authorization.redirect_uri) {
203
+ throw new Error("redirect_uri is required");
204
+ }
205
+ const location = new URL(authorization.redirect_uri);
206
+ location.searchParams.set("code", code);
207
+ if (authorization.state) {
208
+ location.searchParams.set("state", authorization.state);
209
+ }
210
+ await auth.unset(c, "authorization");
211
+ return c.redirect(location.toString(), 302);
212
+ }
213
+ }, {
214
+ provider: currentProvider,
215
+ ...properties && typeof properties === "object" ? properties : {}
216
+ }, c.req.raw, authorization.client_id);
217
+ },
218
+ forward(c, response) {
219
+ return c.newResponse(response.body, response);
220
+ },
221
+ async set(c, key, maxAge, value) {
222
+ const isHttps = isHttpsRequest(c);
223
+ const encryptedValue = await encrypt(value);
224
+ setCookie(c, key, encryptedValue, {
225
+ maxAge,
226
+ httpOnly: true,
227
+ secure: isHttps,
228
+ sameSite: isHttps ? "None" : "Lax",
229
+ path: cookiePath
230
+ });
231
+ },
232
+ async get(c, key) {
233
+ const raw = getCookie(c, key);
234
+ if (!raw) {
235
+ return;
236
+ }
237
+ try {
238
+ const decrypted = await decrypt(raw);
239
+ return decrypted;
240
+ } catch {
241
+ deleteCookie(c, key, {
242
+ path: cookiePath
243
+ });
244
+ return;
245
+ }
246
+ },
247
+ async unset(c, key) {
248
+ deleteCookie(c, key, {
249
+ path: cookiePath
250
+ });
251
+ },
252
+ async invalidate(subject) {
253
+ for await (const [key] of Storage.scan(storage, ["oauth:refresh", subject])) {
254
+ await Storage.remove(storage, key);
255
+ }
256
+ },
257
+ storage
258
+ };
259
+ const app = new Hono({
260
+ strict: false
261
+ }).basePath(input.basePath || "/");
262
+ for (const [name, value] of Object.entries(input.providers)) {
263
+ const route = new Hono;
264
+ route.use(async (c, next) => {
265
+ c.set("provider", name);
266
+ await next();
267
+ });
268
+ value.init(route, {
269
+ name,
270
+ ...auth
271
+ });
272
+ app.route(`/${name}`, route);
273
+ }
274
+ app.get("/.well-known/jwks.json", cors({
275
+ origin: "*",
276
+ allowHeaders: ["*"],
277
+ allowMethods: ["GET"],
278
+ credentials: false
279
+ }), async (c) => {
280
+ const signingKeysData = await allSigning();
281
+ const jwksDocument = {
282
+ keys: signingKeysData.map((keyInfo) => ({
283
+ ...keyInfo.jwk,
284
+ alg: keyInfo.alg,
285
+ exp: keyInfo.expired ? Math.floor(keyInfo.expired.getTime() / 1000) : undefined
286
+ }))
287
+ };
288
+ return c.json(jwksDocument);
289
+ });
290
+ app.get("/.well-known/oauth-authorization-server", cors({
291
+ origin: "*",
292
+ allowHeaders: ["*"],
293
+ allowMethods: ["GET"],
294
+ credentials: false
295
+ }), (c) => {
296
+ const iss = issuerUrl(c);
297
+ const oauth2Document = {
298
+ issuer: iss,
299
+ authorization_endpoint: `${iss}/authorize`,
300
+ token_endpoint: `${iss}/token`,
301
+ jwks_uri: `${iss}/.well-known/jwks.json`,
302
+ response_types_supported: ["code", "token"]
303
+ };
304
+ return c.json(oauth2Document);
305
+ });
306
+ app.post("/token", cors({
307
+ origin: "*",
308
+ allowHeaders: ["*"],
309
+ allowMethods: ["POST"],
310
+ credentials: false
311
+ }), async (c) => {
312
+ const form = await c.req.formData();
313
+ const grantType = form.get("grant_type");
314
+ if (grantType === "authorization_code") {
315
+ const code = form.get("code");
316
+ if (!code) {
317
+ const err = new OauthError("invalid_request", "Missing code");
318
+ return c.json(err.toJSON(), 400);
319
+ }
320
+ const key = ["oauth:code", code.toString()];
321
+ const { isValid, payload } = await normalizeTimingAsync(async () => {
322
+ const data = await Storage.get(storage, key);
323
+ const redirectUri = form.get("redirect_uri");
324
+ const clientId = form.get("client_id");
325
+ const valid = !!(data && data.redirectURI === redirectUri && data.clientID === clientId);
326
+ return {
327
+ isValid: valid,
328
+ payload: valid ? data : undefined
329
+ };
330
+ });
331
+ if (!isValid || !payload) {
332
+ const err = new OauthError("invalid_grant", "Authorization code has been used or expired");
333
+ return c.json(err.toJSON(), 400);
334
+ }
335
+ if (payload.pkce) {
336
+ const codeVerifier = form.get("code_verifier")?.toString();
337
+ if (!codeVerifier) {
338
+ const err = new OauthError("invalid_grant", "Missing code_verifier");
339
+ return c.json(err.toJSON(), 400);
340
+ }
341
+ if (!await validatePKCE(codeVerifier, payload.pkce.challenge, payload.pkce.method)) {
342
+ const err = new OauthError("invalid_grant", "Code verifier does not match");
343
+ return c.json(err.toJSON(), 400);
344
+ }
345
+ }
346
+ const tokens = await generateTokens(c, payload);
347
+ await Storage.remove(storage, key);
348
+ const response = {
349
+ access_token: tokens.access,
350
+ token_type: "Bearer",
351
+ expires_in: tokens.expiresIn,
352
+ refresh_token: tokens.refresh
353
+ };
354
+ return c.json(response);
355
+ }
356
+ if (grantType === "refresh_token") {
357
+ const refreshToken = form.get("refresh_token");
358
+ if (!refreshToken) {
359
+ const err = new OauthError("invalid_request", "Missing refresh_token");
360
+ return c.json(err.toJSON(), 400);
361
+ }
362
+ const refreshTokenStr = refreshToken.toString();
363
+ const isRevoked = await Revocation.isRevoked(storage, refreshTokenStr);
364
+ if (isRevoked) {
365
+ const err = new OauthError("invalid_grant", "Refresh token has been revoked");
366
+ return c.json(err.toJSON(), 400);
367
+ }
368
+ const splits = refreshTokenStr.split(":");
369
+ const token = splits.pop();
370
+ if (!token) {
371
+ throw new Error("Invalid refresh token format");
372
+ }
373
+ const subject = splits.join(":");
374
+ const key = ["oauth:refresh", subject, token];
375
+ const payload = await Storage.get(storage, key);
376
+ if (!payload) {
377
+ const err = new OauthError("invalid_grant", "Refresh token has been used or expired");
378
+ return c.json(err.toJSON(), 400);
379
+ }
380
+ if (input.refresh) {
381
+ try {
382
+ const refreshResult = await input.refresh({
383
+ type: payload.type,
384
+ properties: payload.properties,
385
+ subject: payload.subject,
386
+ clientID: payload.clientID,
387
+ scopes: payload.scopes
388
+ }, c.req.raw);
389
+ if (!refreshResult) {
390
+ await auth.invalidate(subject);
391
+ return c.json({
392
+ error: "invalid_grant",
393
+ error_description: "Refresh token is no longer valid"
394
+ }, 400);
395
+ }
396
+ payload.type = refreshResult.type;
397
+ payload.properties = refreshResult.properties;
398
+ if (refreshResult.subject) {
399
+ payload.subject = refreshResult.subject;
400
+ }
401
+ if (refreshResult.scopes) {
402
+ payload.scopes = refreshResult.scopes;
403
+ }
404
+ } catch {
405
+ return c.json({
406
+ error: "server_error",
407
+ error_description: "Internal server error during token refresh"
408
+ }, 500);
409
+ }
410
+ }
411
+ const generateRefreshToken = !payload.timeUsed;
412
+ if (ttlRefreshReuse <= 0) {
413
+ await Storage.remove(storage, key);
414
+ } else if (!payload.timeUsed) {
415
+ payload.timeUsed = Date.now();
416
+ await Storage.set(storage, key, payload, ttlRefreshReuse + ttlRefreshRetention);
417
+ } else if (Date.now() > payload.timeUsed + ttlRefreshReuse * 1000) {
418
+ await auth.invalidate(subject);
419
+ return c.json({
420
+ error: "invalid_grant",
421
+ error_description: "Refresh token has been used or expired"
422
+ }, 400);
423
+ }
424
+ const tokens = await generateTokens(c, {
425
+ type: payload.type,
426
+ properties: payload.properties,
427
+ subject: payload.subject,
428
+ clientID: payload.clientID,
429
+ ttl: {
430
+ access: ttlAccess,
431
+ refresh: ttlRefresh
432
+ }
433
+ }, {
434
+ generateRefreshToken
435
+ });
436
+ const response = {
437
+ access_token: tokens.access,
438
+ token_type: "Bearer",
439
+ refresh_token: tokens.refresh,
440
+ expires_in: tokens.expiresIn
441
+ };
442
+ return c.json(response);
443
+ }
444
+ return c.json({
445
+ error: "unsupported_grant_type",
446
+ error_description: "The authorization grant type is not supported by the authorization server"
447
+ }, 400);
448
+ });
449
+ app.post("/revoke", cors({
450
+ origin: "*",
451
+ allowHeaders: ["Content-Type"],
452
+ allowMethods: ["POST"],
453
+ credentials: false
454
+ }), async (c) => {
455
+ const form = await c.req.formData();
456
+ const token = form.get("token")?.toString();
457
+ const tokenTypeHint = form.get("token_type_hint")?.toString();
458
+ if (!token) {
459
+ const err = new OauthError("invalid_request", "Missing token parameter");
460
+ return c.json(err.toJSON(), 400);
461
+ }
462
+ try {
463
+ if (tokenTypeHint === "refresh_token") {
464
+ const splits2 = token.split(":");
465
+ const tokenPart2 = splits2.pop();
466
+ if (tokenPart2 && splits2.length > 0) {
467
+ const subject = splits2.join(":");
468
+ const key = ["oauth:refresh", subject, tokenPart2];
469
+ const payload = await Storage.get(storage, key);
470
+ if (payload) {
471
+ await Storage.remove(storage, key);
472
+ const expiresAt2 = Date.now() + ttlRefreshRetention * 1000;
473
+ await Revocation.revoke(storage, token, expiresAt2);
474
+ return c.json({});
475
+ }
476
+ }
477
+ } else if (tokenTypeHint === "access_token") {
478
+ const expiresAt2 = Date.now() + ttlAccess * 1000;
479
+ await Revocation.revoke(storage, token, expiresAt2);
480
+ return c.json({});
481
+ }
482
+ const splits = token.split(":");
483
+ const tokenPart = splits.pop();
484
+ if (tokenPart && splits.length > 0) {
485
+ const subject = splits.join(":");
486
+ const key = ["oauth:refresh", subject, tokenPart];
487
+ const payload = await Storage.get(storage, key);
488
+ if (payload) {
489
+ await Storage.remove(storage, key);
490
+ const expiresAt2 = Date.now() + ttlRefreshRetention * 1000;
491
+ await Revocation.revoke(storage, token, expiresAt2);
492
+ return c.json({});
493
+ }
494
+ }
495
+ const expiresAt = Date.now() + ttlAccess * 1000;
496
+ await Revocation.revoke(storage, token, expiresAt);
497
+ return c.json({});
498
+ } catch {
499
+ return c.json({
500
+ error: "server_error",
501
+ error_description: "Token revocation failed"
502
+ }, 500);
503
+ }
504
+ });
505
+ app.get("/authorize", async (c) => {
506
+ const provider = c.req.query("provider");
507
+ const response_type = c.req.query("response_type");
508
+ const redirect_uri = c.req.query("redirect_uri");
509
+ const state = c.req.query("state");
510
+ const client_id = c.req.query("client_id");
511
+ const audience = c.req.query("audience");
512
+ const code_challenge = c.req.query("code_challenge");
513
+ const code_challenge_method = c.req.query("code_challenge_method");
514
+ const scope = c.req.query("scope");
515
+ const scopes = scope ? scope.split(" ").filter(Boolean) : undefined;
516
+ const authorization = {
517
+ response_type,
518
+ redirect_uri,
519
+ state,
520
+ client_id,
521
+ audience,
522
+ scope,
523
+ scopes,
524
+ ...code_challenge && code_challenge_method && {
525
+ pkce: {
526
+ challenge: code_challenge,
527
+ method: code_challenge_method
528
+ }
529
+ }
530
+ };
531
+ c.set("authorization", authorization);
532
+ if (!redirect_uri) {
533
+ return c.text("Missing redirect_uri", 400);
534
+ }
535
+ try {
536
+ const uri = new URL(redirect_uri);
537
+ if (!uri.protocol || !uri.host) {
538
+ return c.text("Invalid redirect_uri format", 400);
539
+ }
540
+ } catch {
541
+ return c.text("Invalid redirect_uri format", 400);
542
+ }
543
+ if (!response_type) {
544
+ throw new MissingParameterError("response_type");
545
+ }
546
+ if (!client_id) {
547
+ throw new MissingParameterError("client_id");
548
+ }
549
+ if (input.start) {
550
+ await input.start(c.req.raw);
551
+ }
552
+ if (!await allow()({
553
+ clientID: client_id,
554
+ redirectURI: redirect_uri,
555
+ audience
556
+ }, c.req.raw)) {
557
+ throw new UnauthorizedClientError(client_id, redirect_uri);
558
+ }
559
+ await auth.set(c, "authorization", 60 * 15, authorization);
560
+ if (provider) {
561
+ return c.redirect(`${provider}/authorize`);
562
+ }
563
+ const availableProviders = Object.keys(input.providers);
564
+ if (availableProviders.length === 1) {
565
+ return c.redirect(`${availableProviders[0]}/authorize`);
566
+ }
567
+ return auth.forward(c, await select()(Object.fromEntries(Object.entries(input.providers).map(([key, value]) => [key, value.type])), c.req.raw));
568
+ });
569
+ app.onError(async (err, c) => {
570
+ if (err instanceof UnknownStateError) {
571
+ return auth.forward(c, await error(err, c.req.raw));
572
+ }
573
+ try {
574
+ const authorization = await getAuthorization(c);
575
+ if (!authorization.redirect_uri) {
576
+ throw new Error("redirect_uri is required");
577
+ }
578
+ const url = new URL(authorization.redirect_uri);
579
+ const oauth = err instanceof OauthError ? err : new OauthError("server_error", err.message);
580
+ url.searchParams.set("error", oauth.error);
581
+ url.searchParams.set("error_description", oauth.description);
582
+ if (authorization.state) {
583
+ url.searchParams.set("state", authorization.state);
584
+ }
585
+ return c.redirect(url.toString());
586
+ } catch {
587
+ return c.json({
588
+ error: "server_error",
589
+ error_description: err.message
590
+ }, 500);
591
+ }
592
+ });
593
+ return app;
594
+ };
595
+ export {
596
+ issuer
597
+ };
File without changes
@@ -0,0 +1,88 @@
1
+ // src/error.ts
2
+ class OauthError extends Error {
3
+ error;
4
+ description;
5
+ constructor(error, description) {
6
+ super(`${error} - ${description}`);
7
+ this.name = "OauthError";
8
+ this.error = error;
9
+ this.description = description;
10
+ }
11
+ toJSON() {
12
+ return {
13
+ error: this.error,
14
+ error_description: this.description
15
+ };
16
+ }
17
+ }
18
+
19
+ class MissingProviderError extends OauthError {
20
+ constructor() {
21
+ super("invalid_request", "Must specify `provider` query parameter if `select` callback on issuer is not specified");
22
+ this.name = "MissingProviderError";
23
+ }
24
+ }
25
+
26
+ class MissingParameterError extends OauthError {
27
+ parameter;
28
+ constructor(parameter) {
29
+ super("invalid_request", `Missing parameter: ${parameter}`);
30
+ this.name = "MissingParameterError";
31
+ this.parameter = parameter;
32
+ }
33
+ }
34
+
35
+ class UnauthorizedClientError extends OauthError {
36
+ clientID;
37
+ constructor(clientID, redirectURI) {
38
+ super("unauthorized_client", `Client ${clientID} is not authorized to use this redirect_uri: ${redirectURI}`);
39
+ this.name = "UnauthorizedClientError";
40
+ this.clientID = clientID;
41
+ }
42
+ }
43
+
44
+ class UnknownStateError extends Error {
45
+ constructor() {
46
+ super("The browser was in an unknown state. This could be because certain cookies expired or the browser was switched in the middle of an authentication flow.");
47
+ this.name = "UnknownStateError";
48
+ }
49
+ }
50
+
51
+ class InvalidSubjectError extends Error {
52
+ constructor() {
53
+ super("Invalid subject");
54
+ this.name = "InvalidSubjectError";
55
+ }
56
+ }
57
+
58
+ class InvalidRefreshTokenError extends Error {
59
+ constructor() {
60
+ super("Invalid refresh token");
61
+ this.name = "InvalidRefreshTokenError";
62
+ }
63
+ }
64
+
65
+ class InvalidAccessTokenError extends Error {
66
+ constructor() {
67
+ super("Invalid access token");
68
+ this.name = "InvalidAccessTokenError";
69
+ }
70
+ }
71
+
72
+ class InvalidAuthorizationCodeError extends Error {
73
+ constructor() {
74
+ super("Invalid authorization code");
75
+ this.name = "InvalidAuthorizationCodeError";
76
+ }
77
+ }
78
+ export {
79
+ UnknownStateError,
80
+ UnauthorizedClientError,
81
+ OauthError,
82
+ MissingProviderError,
83
+ MissingParameterError,
84
+ InvalidSubjectError,
85
+ InvalidRefreshTokenError,
86
+ InvalidAuthorizationCodeError,
87
+ InvalidAccessTokenError
88
+ };
@@ -0,0 +1,5 @@
1
+ // src/index.ts
2
+ import { issuer } from "./core";
3
+ export {
4
+ issuer
5
+ };