@_mustachio/openauth 0.6.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 (192) hide show
  1. package/dist/esm/client.js +186 -0
  2. package/dist/esm/css.d.js +0 -0
  3. package/dist/esm/error.js +73 -0
  4. package/dist/esm/index.js +14 -0
  5. package/dist/esm/issuer.js +558 -0
  6. package/dist/esm/jwt.js +16 -0
  7. package/dist/esm/keys.js +113 -0
  8. package/dist/esm/pkce.js +35 -0
  9. package/dist/esm/provider/apple.js +28 -0
  10. package/dist/esm/provider/arctic.js +43 -0
  11. package/dist/esm/provider/code.js +58 -0
  12. package/dist/esm/provider/cognito.js +16 -0
  13. package/dist/esm/provider/discord.js +15 -0
  14. package/dist/esm/provider/facebook.js +24 -0
  15. package/dist/esm/provider/github.js +15 -0
  16. package/dist/esm/provider/google.js +25 -0
  17. package/dist/esm/provider/index.js +3 -0
  18. package/dist/esm/provider/jumpcloud.js +15 -0
  19. package/dist/esm/provider/keycloak.js +15 -0
  20. package/dist/esm/provider/linkedin.js +15 -0
  21. package/dist/esm/provider/m2m.js +17 -0
  22. package/dist/esm/provider/microsoft.js +24 -0
  23. package/dist/esm/provider/oauth2.js +119 -0
  24. package/dist/esm/provider/oidc.js +69 -0
  25. package/dist/esm/provider/passkey.js +315 -0
  26. package/dist/esm/provider/password.js +306 -0
  27. package/dist/esm/provider/provider.js +10 -0
  28. package/dist/esm/provider/slack.js +15 -0
  29. package/dist/esm/provider/spotify.js +15 -0
  30. package/dist/esm/provider/twitch.js +15 -0
  31. package/dist/esm/provider/x.js +16 -0
  32. package/dist/esm/provider/yahoo.js +15 -0
  33. package/dist/esm/random.js +27 -0
  34. package/dist/esm/storage/aws.js +39 -0
  35. package/dist/esm/storage/cloudflare.js +42 -0
  36. package/dist/esm/storage/dynamo.js +116 -0
  37. package/dist/esm/storage/memory.js +88 -0
  38. package/dist/esm/storage/storage.js +36 -0
  39. package/dist/esm/subject.js +7 -0
  40. package/dist/esm/ui/base.js +407 -0
  41. package/dist/esm/ui/code.js +151 -0
  42. package/dist/esm/ui/form.js +43 -0
  43. package/dist/esm/ui/icon.js +92 -0
  44. package/dist/esm/ui/passkey.js +329 -0
  45. package/dist/esm/ui/password.js +338 -0
  46. package/dist/esm/ui/select.js +187 -0
  47. package/dist/esm/ui/theme.js +115 -0
  48. package/dist/esm/util.js +54 -0
  49. package/dist/types/client.d.ts +466 -0
  50. package/dist/types/client.d.ts.map +1 -0
  51. package/dist/types/error.d.ts +77 -0
  52. package/dist/types/error.d.ts.map +1 -0
  53. package/dist/types/index.d.ts +20 -0
  54. package/dist/types/index.d.ts.map +1 -0
  55. package/dist/types/issuer.d.ts +465 -0
  56. package/dist/types/issuer.d.ts.map +1 -0
  57. package/dist/types/jwt.d.ts +6 -0
  58. package/dist/types/jwt.d.ts.map +1 -0
  59. package/dist/types/keys.d.ts +18 -0
  60. package/dist/types/keys.d.ts.map +1 -0
  61. package/dist/types/pkce.d.ts +7 -0
  62. package/dist/types/pkce.d.ts.map +1 -0
  63. package/dist/types/provider/apple.d.ts +108 -0
  64. package/dist/types/provider/apple.d.ts.map +1 -0
  65. package/dist/types/provider/arctic.d.ts +16 -0
  66. package/dist/types/provider/arctic.d.ts.map +1 -0
  67. package/dist/types/provider/code.d.ts +74 -0
  68. package/dist/types/provider/code.d.ts.map +1 -0
  69. package/dist/types/provider/cognito.d.ts +64 -0
  70. package/dist/types/provider/cognito.d.ts.map +1 -0
  71. package/dist/types/provider/discord.d.ts +38 -0
  72. package/dist/types/provider/discord.d.ts.map +1 -0
  73. package/dist/types/provider/facebook.d.ts +74 -0
  74. package/dist/types/provider/facebook.d.ts.map +1 -0
  75. package/dist/types/provider/github.d.ts +38 -0
  76. package/dist/types/provider/github.d.ts.map +1 -0
  77. package/dist/types/provider/google.d.ts +74 -0
  78. package/dist/types/provider/google.d.ts.map +1 -0
  79. package/dist/types/provider/index.d.ts +4 -0
  80. package/dist/types/provider/index.d.ts.map +1 -0
  81. package/dist/types/provider/jumpcloud.d.ts +38 -0
  82. package/dist/types/provider/jumpcloud.d.ts.map +1 -0
  83. package/dist/types/provider/keycloak.d.ts +67 -0
  84. package/dist/types/provider/keycloak.d.ts.map +1 -0
  85. package/dist/types/provider/linkedin.d.ts +6 -0
  86. package/dist/types/provider/linkedin.d.ts.map +1 -0
  87. package/dist/types/provider/m2m.d.ts +34 -0
  88. package/dist/types/provider/m2m.d.ts.map +1 -0
  89. package/dist/types/provider/microsoft.d.ts +89 -0
  90. package/dist/types/provider/microsoft.d.ts.map +1 -0
  91. package/dist/types/provider/oauth2.d.ts +133 -0
  92. package/dist/types/provider/oauth2.d.ts.map +1 -0
  93. package/dist/types/provider/oidc.d.ts +91 -0
  94. package/dist/types/provider/oidc.d.ts.map +1 -0
  95. package/dist/types/provider/passkey.d.ts +143 -0
  96. package/dist/types/provider/passkey.d.ts.map +1 -0
  97. package/dist/types/provider/password.d.ts +210 -0
  98. package/dist/types/provider/password.d.ts.map +1 -0
  99. package/dist/types/provider/provider.d.ts +29 -0
  100. package/dist/types/provider/provider.d.ts.map +1 -0
  101. package/dist/types/provider/slack.d.ts +59 -0
  102. package/dist/types/provider/slack.d.ts.map +1 -0
  103. package/dist/types/provider/spotify.d.ts +38 -0
  104. package/dist/types/provider/spotify.d.ts.map +1 -0
  105. package/dist/types/provider/twitch.d.ts +38 -0
  106. package/dist/types/provider/twitch.d.ts.map +1 -0
  107. package/dist/types/provider/x.d.ts +38 -0
  108. package/dist/types/provider/x.d.ts.map +1 -0
  109. package/dist/types/provider/yahoo.d.ts +38 -0
  110. package/dist/types/provider/yahoo.d.ts.map +1 -0
  111. package/dist/types/random.d.ts +3 -0
  112. package/dist/types/random.d.ts.map +1 -0
  113. package/dist/types/storage/aws.d.ts +4 -0
  114. package/dist/types/storage/aws.d.ts.map +1 -0
  115. package/dist/types/storage/cloudflare.d.ts +34 -0
  116. package/dist/types/storage/cloudflare.d.ts.map +1 -0
  117. package/dist/types/storage/dynamo.d.ts +65 -0
  118. package/dist/types/storage/dynamo.d.ts.map +1 -0
  119. package/dist/types/storage/memory.d.ts +49 -0
  120. package/dist/types/storage/memory.d.ts.map +1 -0
  121. package/dist/types/storage/storage.d.ts +15 -0
  122. package/dist/types/storage/storage.d.ts.map +1 -0
  123. package/dist/types/subject.d.ts +122 -0
  124. package/dist/types/subject.d.ts.map +1 -0
  125. package/dist/types/ui/base.d.ts +5 -0
  126. package/dist/types/ui/base.d.ts.map +1 -0
  127. package/dist/types/ui/code.d.ts +104 -0
  128. package/dist/types/ui/code.d.ts.map +1 -0
  129. package/dist/types/ui/form.d.ts +6 -0
  130. package/dist/types/ui/form.d.ts.map +1 -0
  131. package/dist/types/ui/icon.d.ts +6 -0
  132. package/dist/types/ui/icon.d.ts.map +1 -0
  133. package/dist/types/ui/passkey.d.ts +5 -0
  134. package/dist/types/ui/passkey.d.ts.map +1 -0
  135. package/dist/types/ui/password.d.ts +139 -0
  136. package/dist/types/ui/password.d.ts.map +1 -0
  137. package/dist/types/ui/select.d.ts +55 -0
  138. package/dist/types/ui/select.d.ts.map +1 -0
  139. package/dist/types/ui/theme.d.ts +207 -0
  140. package/dist/types/ui/theme.d.ts.map +1 -0
  141. package/dist/types/util.d.ts +8 -0
  142. package/dist/types/util.d.ts.map +1 -0
  143. package/package.json +51 -0
  144. package/src/client.ts +749 -0
  145. package/src/css.d.ts +4 -0
  146. package/src/error.ts +120 -0
  147. package/src/index.ts +26 -0
  148. package/src/issuer.ts +1302 -0
  149. package/src/jwt.ts +17 -0
  150. package/src/keys.ts +139 -0
  151. package/src/pkce.ts +40 -0
  152. package/src/provider/apple.ts +127 -0
  153. package/src/provider/arctic.ts +66 -0
  154. package/src/provider/code.ts +227 -0
  155. package/src/provider/cognito.ts +74 -0
  156. package/src/provider/discord.ts +45 -0
  157. package/src/provider/facebook.ts +84 -0
  158. package/src/provider/github.ts +45 -0
  159. package/src/provider/google.ts +85 -0
  160. package/src/provider/index.ts +3 -0
  161. package/src/provider/jumpcloud.ts +45 -0
  162. package/src/provider/keycloak.ts +75 -0
  163. package/src/provider/linkedin.ts +12 -0
  164. package/src/provider/m2m.ts +56 -0
  165. package/src/provider/microsoft.ts +100 -0
  166. package/src/provider/oauth2.ts +297 -0
  167. package/src/provider/oidc.ts +179 -0
  168. package/src/provider/passkey.ts +655 -0
  169. package/src/provider/password.ts +672 -0
  170. package/src/provider/provider.ts +33 -0
  171. package/src/provider/slack.ts +67 -0
  172. package/src/provider/spotify.ts +45 -0
  173. package/src/provider/twitch.ts +45 -0
  174. package/src/provider/x.ts +46 -0
  175. package/src/provider/yahoo.ts +45 -0
  176. package/src/random.ts +24 -0
  177. package/src/storage/aws.ts +59 -0
  178. package/src/storage/cloudflare.ts +77 -0
  179. package/src/storage/dynamo.ts +193 -0
  180. package/src/storage/memory.ts +135 -0
  181. package/src/storage/storage.ts +46 -0
  182. package/src/subject.ts +130 -0
  183. package/src/ui/base.tsx +118 -0
  184. package/src/ui/code.tsx +215 -0
  185. package/src/ui/form.tsx +40 -0
  186. package/src/ui/icon.tsx +95 -0
  187. package/src/ui/passkey.tsx +321 -0
  188. package/src/ui/password.tsx +405 -0
  189. package/src/ui/select.tsx +221 -0
  190. package/src/ui/theme.ts +319 -0
  191. package/src/ui/ui.css +252 -0
  192. package/src/util.ts +58 -0
@@ -0,0 +1,315 @@
1
+ // src/provider/passkey.ts
2
+ import {
3
+ generateRegistrationOptions,
4
+ verifyRegistrationResponse,
5
+ generateAuthenticationOptions,
6
+ verifyAuthenticationResponse
7
+ } from "@simplewebauthn/server";
8
+ import { Storage } from "../storage/storage.js";
9
+ function uint8ArrayToBase64Url(bytes) {
10
+ let str = "";
11
+ for (const charCode of bytes) {
12
+ str += String.fromCharCode(charCode);
13
+ }
14
+ const base64String = btoa(str);
15
+ return base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
16
+ }
17
+ function base64UrlToUint8Array(base64urlString) {
18
+ const base64 = base64urlString.replace(/-/g, "+").replace(/_/g, "/");
19
+ const padLength = (4 - base64.length % 4) % 4;
20
+ const padded = base64.padEnd(base64.length + padLength, "=");
21
+ const binary = atob(padded);
22
+ const buffer = new ArrayBuffer(binary.length);
23
+ const bytes = new Uint8Array(buffer);
24
+ for (let i = 0;i < binary.length; i++) {
25
+ bytes[i] = binary.charCodeAt(i);
26
+ }
27
+ return bytes;
28
+ }
29
+ var userKey = (userId) => ["passkey", "user", userId];
30
+ var passkeyKey = (userId, credentialId) => [
31
+ "passkey",
32
+ "user",
33
+ userId,
34
+ "credential",
35
+ credentialId,
36
+ "passkey"
37
+ ];
38
+ var optionsKey = (userId) => ["passkey", "user", userId, "options"];
39
+ var userPasskeysIndexKey = (userId) => [
40
+ "passkey",
41
+ "user",
42
+ userId,
43
+ "passkeys"
44
+ ];
45
+ var DEFAULT_COPY = {
46
+ error_user_not_allowed: "There is already an account with this email. Login to add a passkey."
47
+ };
48
+ function PasskeyProvider(config) {
49
+ const copy = {
50
+ ...DEFAULT_COPY,
51
+ ...config.copy
52
+ };
53
+ return {
54
+ type: "passkey",
55
+ init(routes, ctx) {
56
+ const {
57
+ rpName,
58
+ authenticatorSelection,
59
+ attestationType = "none",
60
+ timeout = 5 * 60 * 1000
61
+ } = config;
62
+ async function getStoredUserById(userId) {
63
+ return await Storage.get(ctx.storage, userKey(userId));
64
+ }
65
+ async function saveUser(user) {
66
+ await Storage.set(ctx.storage, userKey(user.id), user);
67
+ }
68
+ async function getStoredPasskeyById(userId, credentialID) {
69
+ const storedPasskey = await Storage.get(ctx.storage, passkeyKey(userId, credentialID));
70
+ if (!storedPasskey)
71
+ return null;
72
+ return {
73
+ ...storedPasskey,
74
+ publicKey: base64UrlToUint8Array(storedPasskey.publicKey)
75
+ };
76
+ }
77
+ async function getStoredUserPasskeys(userId) {
78
+ const passkeyIds = await Storage.get(ctx.storage, userPasskeysIndexKey(userId)) || [];
79
+ const passkeys = [];
80
+ for (const id of passkeyIds) {
81
+ const pk = await getStoredPasskeyById(userId, id);
82
+ if (pk)
83
+ passkeys.push(pk);
84
+ }
85
+ return passkeys;
86
+ }
87
+ async function saveNewStoredPasskey(passkeyData) {
88
+ const storablePasskey = {
89
+ ...passkeyData,
90
+ publicKey: uint8ArrayToBase64Url(passkeyData.publicKey)
91
+ };
92
+ await Storage.set(ctx.storage, passkeyKey(passkeyData.userId, passkeyData.id), storablePasskey);
93
+ const passkeyIds = await Storage.get(ctx.storage, userPasskeysIndexKey(passkeyData.userId)) || [];
94
+ if (!passkeyIds.includes(passkeyData.id)) {
95
+ passkeyIds.push(passkeyData.id);
96
+ await Storage.set(ctx.storage, userPasskeysIndexKey(passkeyData.userId), passkeyIds);
97
+ }
98
+ }
99
+ async function updateStoredPasskeyCounter(userId, credentialID, newCounter) {
100
+ const passkey = await getStoredPasskeyById(userId, credentialID);
101
+ if (passkey) {
102
+ passkey.counter = newCounter;
103
+ const storablePasskey = {
104
+ ...passkey,
105
+ publicKey: uint8ArrayToBase64Url(passkey.publicKey)
106
+ };
107
+ await Storage.set(ctx.storage, passkeyKey(userId, credentialID), storablePasskey);
108
+ }
109
+ }
110
+ routes.get("/authorize", async (c) => {
111
+ return ctx.forward(c, await config.authorize(c.req.raw));
112
+ });
113
+ routes.get("/register", async (c) => {
114
+ return ctx.forward(c, await config.register(c.req.raw));
115
+ });
116
+ routes.get("/register-request", async (c) => {
117
+ const userId = c.req.query("userId");
118
+ const rpID = config.rpID || c.req.query("rpID");
119
+ const otherDevice = c.req.query("otherDevice") === "true";
120
+ if (!userId) {
121
+ return c.json({ error: "User ID for registration is required." }, 400);
122
+ }
123
+ if (!rpID) {
124
+ return c.json({ error: "RP ID for registration is required." }, 400);
125
+ }
126
+ const username = c.req.query("username") || userId;
127
+ let user = await getStoredUserById(userId);
128
+ if (config.userCanRegisterPasskey) {
129
+ const isAllowed = await config.userCanRegisterPasskey(userId, c.req.raw);
130
+ if (!isAllowed) {
131
+ return c.json({
132
+ error: copy.error_user_not_allowed
133
+ }, 403);
134
+ }
135
+ }
136
+ if (!user) {
137
+ user = { id: userId, username };
138
+ await saveUser(user);
139
+ }
140
+ const userPasskeys = await getStoredUserPasskeys(user.id);
141
+ const regOptions = await generateRegistrationOptions({
142
+ rpName,
143
+ rpID,
144
+ userName: user.username,
145
+ attestationType,
146
+ excludeCredentials: userPasskeys.map((pk) => ({
147
+ id: pk.id,
148
+ transports: pk.transports
149
+ })),
150
+ authenticatorSelection: authenticatorSelection ?? {
151
+ residentKey: "preferred",
152
+ userVerification: "preferred",
153
+ authenticatorAttachment: otherDevice ? "cross-platform" : "platform"
154
+ },
155
+ timeout
156
+ });
157
+ await Storage.set(ctx.storage, optionsKey(user.id), regOptions);
158
+ return c.json(regOptions);
159
+ });
160
+ routes.post("/register-verify", async (c) => {
161
+ const body = await c.req.json();
162
+ const { userId } = c.req.query();
163
+ const rpID = config.rpID || c.req.query("rpID");
164
+ const origin = config.origin || c.req.query("origin");
165
+ if (!userId) {
166
+ return c.json({
167
+ verified: false,
168
+ error: "User ID for verification is required."
169
+ }, 400);
170
+ }
171
+ if (!rpID) {
172
+ return c.json({ error: "RP ID for verification is required." }, 400);
173
+ }
174
+ if (!origin) {
175
+ return c.json({ error: "Origin for verification is required." }, 400);
176
+ }
177
+ const user = await getStoredUserById(userId);
178
+ if (!user) {
179
+ return c.json({ verified: false, error: "User not found during verification." }, 404);
180
+ }
181
+ const regOptions = await Storage.get(ctx.storage, optionsKey(user.id));
182
+ if (!regOptions) {
183
+ return c.json({ verified: false, error: "Registration options not found." }, 400);
184
+ }
185
+ const challenge = regOptions.challenge;
186
+ let verification;
187
+ try {
188
+ verification = await verifyRegistrationResponse({
189
+ response: body,
190
+ expectedChallenge: challenge,
191
+ expectedOrigin: origin,
192
+ expectedRPID: rpID,
193
+ requireUserVerification: authenticatorSelection?.userVerification !== "discouraged"
194
+ });
195
+ } catch (error) {
196
+ console.error("Passkey Registration Verification Error:", error);
197
+ return c.json({ verified: false, error: error.message }, 400);
198
+ }
199
+ const { verified, registrationInfo } = verification;
200
+ if (verified && registrationInfo) {
201
+ const { credential, credentialDeviceType, credentialBackedUp } = registrationInfo;
202
+ if (credential) {
203
+ const newPasskey = {
204
+ id: credential.id,
205
+ userId: user.id,
206
+ webauthnUserID: regOptions.user.id,
207
+ publicKey: credential.publicKey,
208
+ counter: credential.counter,
209
+ transports: credential.transports,
210
+ deviceType: credentialDeviceType,
211
+ backedUp: credentialBackedUp
212
+ };
213
+ await saveNewStoredPasskey(newPasskey);
214
+ return ctx.success(c, {
215
+ userId: user.id,
216
+ credentialId: newPasskey.id,
217
+ verified: true
218
+ });
219
+ }
220
+ }
221
+ return c.json({ verified: false, error: "Registration verification failed." }, 400);
222
+ });
223
+ routes.get("/authenticate-options", async (c) => {
224
+ const { userId } = c.req.query();
225
+ if (!userId) {
226
+ return c.json({ error: "User ID for authentication is required." }, 400);
227
+ }
228
+ const rpID = config.rpID || c.req.query("rpID");
229
+ if (!rpID) {
230
+ return c.json({ error: "RP ID for authentication is required." }, 400);
231
+ }
232
+ const userForAuth = await getStoredUserById(userId);
233
+ if (!userForAuth) {
234
+ return c.json({ error: "User not found for authentication." }, 404);
235
+ }
236
+ const userPasskeys = await getStoredUserPasskeys(userForAuth.id);
237
+ const allowCredentialsList = userPasskeys.map((pk) => ({
238
+ id: pk.id,
239
+ transports: pk.transports
240
+ }));
241
+ const authOptions = await generateAuthenticationOptions({
242
+ rpID,
243
+ allowCredentials: allowCredentialsList,
244
+ userVerification: authenticatorSelection?.userVerification ?? "preferred",
245
+ timeout
246
+ });
247
+ await Storage.set(ctx.storage, optionsKey(userForAuth.id), authOptions);
248
+ return c.json(authOptions);
249
+ });
250
+ routes.post("/authenticate-verify", async (c) => {
251
+ const body = await c.req.json();
252
+ const { userId } = c.req.query();
253
+ if (!userId) {
254
+ return c.json({ error: "User ID for authentication is required." }, 400);
255
+ }
256
+ const rpID = config.rpID || c.req.query("rpID");
257
+ if (!rpID) {
258
+ return c.json({ error: "RP ID for authentication is required." }, 400);
259
+ }
260
+ const origin = config.origin || c.req.query("origin");
261
+ if (!origin) {
262
+ return c.json({ error: "Origin for authentication is required." }, 400);
263
+ }
264
+ const user = await getStoredUserById(userId);
265
+ if (!user) {
266
+ return c.json({ verified: false, error: `User ${userId} not found.` }, 404);
267
+ }
268
+ const authOptions = await Storage.get(ctx.storage, optionsKey(user.id));
269
+ if (!authOptions) {
270
+ return c.json({ error: "Authentication options not found." }, 400);
271
+ }
272
+ const passkey = await getStoredPasskeyById(userId, body.id);
273
+ if (!passkey) {
274
+ return c.json({
275
+ verified: false,
276
+ error: `Passkey ${body.id} not found for user ${user.username}.`
277
+ }, 400);
278
+ }
279
+ const { publicKey, counter, transports } = passkey;
280
+ if (!publicKey || typeof counter !== "number" || !transports) {
281
+ return c.json({ error: "Passkey not found for authentication." }, 400);
282
+ }
283
+ const challenge = authOptions.challenge;
284
+ if (!challenge) {
285
+ return c.json({ error: "Authentication challenge not found." }, 400);
286
+ }
287
+ const verification = await verifyAuthenticationResponse({
288
+ response: body,
289
+ expectedChallenge: challenge,
290
+ expectedOrigin: origin || "",
291
+ expectedRPID: rpID,
292
+ credential: {
293
+ id: passkey.id,
294
+ publicKey,
295
+ counter,
296
+ transports
297
+ }
298
+ });
299
+ const { verified, authenticationInfo } = verification;
300
+ if (verified) {
301
+ await updateStoredPasskeyCounter(user.id, passkey.id, authenticationInfo.newCounter);
302
+ return ctx.success(c, {
303
+ userId: user.id,
304
+ credentialId: passkey.id,
305
+ verified: true
306
+ });
307
+ }
308
+ return c.json({ verified: false, error: "Authentication verification failed." }, 400);
309
+ });
310
+ }
311
+ };
312
+ }
313
+ export {
314
+ PasskeyProvider
315
+ };
@@ -0,0 +1,306 @@
1
+ // src/provider/password.ts
2
+ import { UnknownStateError } from "../error.js";
3
+ import { Storage } from "../storage/storage.js";
4
+ import { generateUnbiasedDigits, timingSafeCompare } from "../random.js";
5
+ import * as jose from "jose";
6
+ import { TextEncoder } from "node:util";
7
+ import { timingSafeEqual, randomBytes, scrypt } from "node:crypto";
8
+ import { getRelativeUrl } from "../util.js";
9
+ function PasswordProvider(config) {
10
+ const hasher = config.hasher ?? ScryptHasher();
11
+ function generate() {
12
+ return generateUnbiasedDigits(6);
13
+ }
14
+ return {
15
+ type: "password",
16
+ init(routes, ctx) {
17
+ routes.get("/authorize", async (c) => ctx.forward(c, await config.login(c.req.raw)));
18
+ routes.post("/authorize", async (c) => {
19
+ const fd = await c.req.formData();
20
+ async function error(err) {
21
+ return ctx.forward(c, await config.login(c.req.raw, fd, err));
22
+ }
23
+ const email = fd.get("email")?.toString()?.toLowerCase();
24
+ if (!email)
25
+ return error({ type: "invalid_email" });
26
+ const hash = await Storage.get(ctx.storage, [
27
+ "email",
28
+ email,
29
+ "password"
30
+ ]);
31
+ const password = fd.get("password")?.toString();
32
+ if (!password || !hash || !await hasher.verify(password, hash))
33
+ return error({ type: "invalid_password" });
34
+ return ctx.success(c, {
35
+ email
36
+ }, {
37
+ invalidate: async (subject) => {
38
+ await Storage.set(ctx.storage, ["email", email, "subject"], subject);
39
+ }
40
+ });
41
+ });
42
+ routes.get("/register", async (c) => {
43
+ const state = {
44
+ type: "start"
45
+ };
46
+ await ctx.set(c, "provider", 60 * 60 * 24, state);
47
+ return ctx.forward(c, await config.register(c.req.raw, state));
48
+ });
49
+ routes.post("/register", async (c) => {
50
+ const fd = await c.req.formData();
51
+ const email = fd.get("email")?.toString()?.toLowerCase();
52
+ const action = fd.get("action")?.toString();
53
+ const provider = await ctx.get(c, "provider");
54
+ async function transition(next, err) {
55
+ await ctx.set(c, "provider", 60 * 60 * 24, next);
56
+ return ctx.forward(c, await config.register(c.req.raw, next, fd, err));
57
+ }
58
+ if (action === "register" && provider.type === "start") {
59
+ const password = fd.get("password")?.toString();
60
+ const repeat = fd.get("repeat")?.toString();
61
+ if (!email)
62
+ return transition(provider, { type: "invalid_email" });
63
+ if (!password)
64
+ return transition(provider, { type: "invalid_password" });
65
+ if (password !== repeat)
66
+ return transition(provider, { type: "password_mismatch" });
67
+ if (config.validatePassword) {
68
+ let validationError;
69
+ try {
70
+ if (typeof config.validatePassword === "function") {
71
+ validationError = await config.validatePassword(password);
72
+ } else {
73
+ const res = await config.validatePassword["~standard"].validate(password);
74
+ if (res.issues?.length) {
75
+ throw new Error(res.issues.map((issue) => issue.message).join(", "));
76
+ }
77
+ }
78
+ } catch (error) {
79
+ validationError = error instanceof Error ? error.message : undefined;
80
+ }
81
+ if (validationError)
82
+ return transition(provider, {
83
+ type: "validation_error",
84
+ message: validationError
85
+ });
86
+ }
87
+ const existing = await Storage.get(ctx.storage, [
88
+ "email",
89
+ email,
90
+ "password"
91
+ ]);
92
+ if (existing)
93
+ return transition(provider, { type: "email_taken" });
94
+ const code = generate();
95
+ await config.sendCode(email, code);
96
+ return transition({
97
+ type: "code",
98
+ code,
99
+ password: await hasher.hash(password),
100
+ email
101
+ });
102
+ }
103
+ if (action === "register" && provider.type === "code") {
104
+ const code = generate();
105
+ await config.sendCode(provider.email, code);
106
+ return transition({
107
+ type: "code",
108
+ code,
109
+ password: provider.password,
110
+ email: provider.email
111
+ });
112
+ }
113
+ if (action === "verify" && provider.type === "code") {
114
+ const code = fd.get("code")?.toString();
115
+ if (!code || !timingSafeCompare(code, provider.code))
116
+ return transition(provider, { type: "invalid_code" });
117
+ const existing = await Storage.get(ctx.storage, [
118
+ "email",
119
+ provider.email,
120
+ "password"
121
+ ]);
122
+ if (existing)
123
+ return transition({ type: "start" }, { type: "email_taken" });
124
+ await Storage.set(ctx.storage, ["email", provider.email, "password"], provider.password);
125
+ return ctx.success(c, {
126
+ email: provider.email
127
+ });
128
+ }
129
+ return transition({ type: "start" });
130
+ });
131
+ routes.get("/change", async (c) => {
132
+ let redirect = c.req.query("redirect_uri") || getRelativeUrl(c, "./authorize");
133
+ const state = {
134
+ type: "start",
135
+ redirect
136
+ };
137
+ await ctx.set(c, "provider", 60 * 60 * 24, state);
138
+ return ctx.forward(c, await config.change(c.req.raw, state));
139
+ });
140
+ routes.post("/change", async (c) => {
141
+ const fd = await c.req.formData();
142
+ const action = fd.get("action")?.toString();
143
+ const provider = await ctx.get(c, "provider");
144
+ if (!provider)
145
+ throw new UnknownStateError;
146
+ async function transition(next, err) {
147
+ await ctx.set(c, "provider", 60 * 60 * 24, next);
148
+ return ctx.forward(c, await config.change(c.req.raw, next, fd, err));
149
+ }
150
+ if (action === "code") {
151
+ const email = fd.get("email")?.toString()?.toLowerCase();
152
+ if (!email)
153
+ return transition({ type: "start", redirect: provider.redirect }, { type: "invalid_email" });
154
+ const code = generate();
155
+ await config.sendCode(email, code);
156
+ return transition({
157
+ type: "code",
158
+ code,
159
+ email,
160
+ redirect: provider.redirect
161
+ });
162
+ }
163
+ if (action === "verify" && provider.type === "code") {
164
+ const code = fd.get("code")?.toString();
165
+ if (!code || !timingSafeCompare(code, provider.code))
166
+ return transition(provider, { type: "invalid_code" });
167
+ return transition({
168
+ type: "update",
169
+ email: provider.email,
170
+ redirect: provider.redirect
171
+ });
172
+ }
173
+ if (action === "update" && provider.type === "update") {
174
+ const existing = await Storage.get(ctx.storage, [
175
+ "email",
176
+ provider.email,
177
+ "password"
178
+ ]);
179
+ if (!existing)
180
+ return c.redirect(provider.redirect, 302);
181
+ const password = fd.get("password")?.toString();
182
+ const repeat = fd.get("repeat")?.toString();
183
+ if (!password)
184
+ return transition(provider, { type: "invalid_password" });
185
+ if (password !== repeat)
186
+ return transition(provider, { type: "password_mismatch" });
187
+ if (config.validatePassword) {
188
+ let validationError;
189
+ try {
190
+ if (typeof config.validatePassword === "function") {
191
+ validationError = await config.validatePassword(password);
192
+ } else {
193
+ const res = await config.validatePassword["~standard"].validate(password);
194
+ if (res.issues?.length) {
195
+ throw new Error(res.issues.map((issue) => issue.message).join(", "));
196
+ }
197
+ }
198
+ } catch (error) {
199
+ validationError = error instanceof Error ? error.message : undefined;
200
+ }
201
+ if (validationError)
202
+ return transition(provider, {
203
+ type: "validation_error",
204
+ message: validationError
205
+ });
206
+ }
207
+ await Storage.set(ctx.storage, ["email", provider.email, "password"], await hasher.hash(password));
208
+ const subject = await Storage.get(ctx.storage, [
209
+ "email",
210
+ provider.email,
211
+ "subject"
212
+ ]);
213
+ if (subject)
214
+ await ctx.invalidate(subject);
215
+ return c.redirect(provider.redirect, 302);
216
+ }
217
+ return transition({ type: "start", redirect: provider.redirect });
218
+ });
219
+ }
220
+ };
221
+ }
222
+ function PBKDF2Hasher(opts) {
223
+ const iterations = opts?.iterations ?? 600000;
224
+ return {
225
+ async hash(password) {
226
+ const encoder = new TextEncoder;
227
+ const bytes = encoder.encode(password);
228
+ const salt = crypto.getRandomValues(new Uint8Array(16));
229
+ const keyMaterial = await crypto.subtle.importKey("raw", bytes, "PBKDF2", false, ["deriveBits"]);
230
+ const hash = await crypto.subtle.deriveBits({
231
+ name: "PBKDF2",
232
+ hash: "SHA-256",
233
+ salt,
234
+ iterations
235
+ }, keyMaterial, 256);
236
+ const hashBase64 = jose.base64url.encode(new Uint8Array(hash));
237
+ const saltBase64 = jose.base64url.encode(salt);
238
+ return {
239
+ hash: hashBase64,
240
+ salt: saltBase64,
241
+ iterations
242
+ };
243
+ },
244
+ async verify(password, compare) {
245
+ const encoder = new TextEncoder;
246
+ const passwordBytes = encoder.encode(password);
247
+ const salt = jose.base64url.decode(compare.salt);
248
+ const params = {
249
+ name: "PBKDF2",
250
+ hash: "SHA-256",
251
+ salt,
252
+ iterations: compare.iterations
253
+ };
254
+ const keyMaterial = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits"]);
255
+ const hash = await crypto.subtle.deriveBits(params, keyMaterial, 256);
256
+ const hashBase64 = jose.base64url.encode(new Uint8Array(hash));
257
+ return hashBase64 === compare.hash;
258
+ }
259
+ };
260
+ }
261
+ function ScryptHasher(opts) {
262
+ const N = opts?.N ?? 16384;
263
+ const r = opts?.r ?? 8;
264
+ const p = opts?.p ?? 1;
265
+ return {
266
+ async hash(password) {
267
+ const salt = randomBytes(16);
268
+ const keyLength = 32;
269
+ const derivedKey = await new Promise((resolve, reject) => {
270
+ scrypt(password, salt, keyLength, { N, r, p }, (err, derivedKey2) => {
271
+ if (err)
272
+ reject(err);
273
+ else
274
+ resolve(derivedKey2);
275
+ });
276
+ });
277
+ const hashBase64 = derivedKey.toString("base64");
278
+ const saltBase64 = salt.toString("base64");
279
+ return {
280
+ hash: hashBase64,
281
+ salt: saltBase64,
282
+ N,
283
+ r,
284
+ p
285
+ };
286
+ },
287
+ async verify(password, compare) {
288
+ const salt = Buffer.from(compare.salt, "base64");
289
+ const keyLength = 32;
290
+ const derivedKey = await new Promise((resolve, reject) => {
291
+ scrypt(password, salt, keyLength, { N: compare.N, r: compare.r, p: compare.p }, (err, derivedKey2) => {
292
+ if (err)
293
+ reject(err);
294
+ else
295
+ resolve(derivedKey2);
296
+ });
297
+ });
298
+ return timingSafeEqual(derivedKey, Buffer.from(compare.hash, "base64"));
299
+ }
300
+ };
301
+ }
302
+ export {
303
+ ScryptHasher,
304
+ PasswordProvider,
305
+ PBKDF2Hasher
306
+ };
@@ -0,0 +1,10 @@
1
+ // src/provider/provider.ts
2
+ class ProviderError extends Error {
3
+ }
4
+
5
+ class ProviderUnknownError extends ProviderError {
6
+ }
7
+ export {
8
+ ProviderUnknownError,
9
+ ProviderError
10
+ };
@@ -0,0 +1,15 @@
1
+ // src/provider/slack.ts
2
+ import { Oauth2Provider } from "./oauth2.js";
3
+ function SlackProvider(config) {
4
+ return Oauth2Provider({
5
+ ...config,
6
+ type: "slack",
7
+ endpoint: {
8
+ authorization: "https://slack.com/openid/connect/authorize",
9
+ token: "https://slack.com/api/openid.connect.token"
10
+ }
11
+ });
12
+ }
13
+ export {
14
+ SlackProvider
15
+ };
@@ -0,0 +1,15 @@
1
+ // src/provider/spotify.ts
2
+ import { Oauth2Provider } from "./oauth2.js";
3
+ function SpotifyProvider(config) {
4
+ return Oauth2Provider({
5
+ ...config,
6
+ type: "spotify",
7
+ endpoint: {
8
+ authorization: "https://accounts.spotify.com/authorize",
9
+ token: "https://accounts.spotify.com/api/token"
10
+ }
11
+ });
12
+ }
13
+ export {
14
+ SpotifyProvider
15
+ };
@@ -0,0 +1,15 @@
1
+ // src/provider/twitch.ts
2
+ import { Oauth2Provider } from "./oauth2.js";
3
+ function TwitchProvider(config) {
4
+ return Oauth2Provider({
5
+ type: "twitch",
6
+ ...config,
7
+ endpoint: {
8
+ authorization: "https://id.twitch.tv/oauth2/authorize",
9
+ token: "https://id.twitch.tv/oauth2/token"
10
+ }
11
+ });
12
+ }
13
+ export {
14
+ TwitchProvider
15
+ };
@@ -0,0 +1,16 @@
1
+ // src/provider/x.ts
2
+ import { Oauth2Provider } from "./oauth2.js";
3
+ function XProvider(config) {
4
+ return Oauth2Provider({
5
+ ...config,
6
+ type: "x",
7
+ endpoint: {
8
+ authorization: "https://twitter.com/i/oauth2/authorize",
9
+ token: "https://api.x.com/2/oauth2/token"
10
+ },
11
+ pkce: true
12
+ });
13
+ }
14
+ export {
15
+ XProvider
16
+ };