@unchainedshop/core-users 3.0.0-alpha6 → 3.0.0-rc.11

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 (63) hide show
  1. package/lib/db/UsersCollection.d.ts +54 -2
  2. package/lib/db/UsersCollection.d.ts.map +1 -1
  3. package/lib/db/UsersCollection.js.map +1 -1
  4. package/lib/db/WebAuthnCredentialsCreationRequestsCollection.d.ts +6 -1
  5. package/lib/db/WebAuthnCredentialsCreationRequestsCollection.d.ts.map +1 -1
  6. package/lib/db/WebAuthnCredentialsCreationRequestsCollection.js.map +1 -1
  7. package/lib/module/addMigrations.d.ts.map +1 -1
  8. package/lib/module/addMigrations.js +32 -41
  9. package/lib/module/addMigrations.js.map +1 -1
  10. package/lib/module/configureUsersModule.d.ts +49 -70
  11. package/lib/module/configureUsersModule.d.ts.map +1 -1
  12. package/lib/module/configureUsersModule.js +106 -71
  13. package/lib/module/configureUsersModule.js.map +1 -1
  14. package/lib/module/configureUsersWebAuthnModule.d.ts +21 -8
  15. package/lib/module/configureUsersWebAuthnModule.d.ts.map +1 -1
  16. package/lib/module/configureUsersWebAuthnModule.js +32 -11
  17. package/lib/module/configureUsersWebAuthnModule.js.map +1 -1
  18. package/lib/module/pbkdf2.js +1 -1
  19. package/lib/module/pbkdf2.js.map +1 -1
  20. package/lib/users-index.d.ts +3 -2
  21. package/lib/users-index.d.ts.map +1 -1
  22. package/lib/users-index.js +3 -2
  23. package/lib/users-index.js.map +1 -1
  24. package/lib/users-settings.d.ts +1 -1
  25. package/lib/users-settings.d.ts.map +1 -1
  26. package/lib/users-settings.js.map +1 -1
  27. package/package.json +26 -18
  28. package/src/db/UsersCollection.ts +59 -2
  29. package/src/db/WebAuthnCredentialsCreationRequestsCollection.ts +7 -1
  30. package/src/module/addMigrations.ts +40 -53
  31. package/src/module/configureUsersModule.ts +153 -169
  32. package/src/module/configureUsersWebAuthnModule.ts +47 -44
  33. package/src/module/pbkdf2.ts +1 -1
  34. package/src/module/removeConfidentialServiceHashes.test.ts +1 -1
  35. package/src/users-index.ts +3 -2
  36. package/src/users-settings.ts +1 -2
  37. package/tests/mock/user-mock.ts +2 -3
  38. package/tsconfig.json +5 -7
  39. package/lib/module/sha256.d.ts +0 -2
  40. package/lib/module/sha256.d.ts.map +0 -1
  41. package/lib/module/sha256.js +0 -6
  42. package/lib/module/sha256.js.map +0 -1
  43. package/lib/services/migrateUserDataService.d.ts +0 -4
  44. package/lib/services/migrateUserDataService.d.ts.map +0 -1
  45. package/lib/services/migrateUserDataService.js +0 -18
  46. package/lib/services/migrateUserDataService.js.map +0 -1
  47. package/lib/services/updateUserAvatarAfterUploadService.d.ts +0 -15
  48. package/lib/services/updateUserAvatarAfterUploadService.d.ts.map +0 -1
  49. package/lib/services/updateUserAvatarAfterUploadService.js +0 -24
  50. package/lib/services/updateUserAvatarAfterUploadService.js.map +0 -1
  51. package/lib/services/userServices.d.ts +0 -8
  52. package/lib/services/userServices.d.ts.map +0 -1
  53. package/lib/services/userServices.js +0 -7
  54. package/lib/services/userServices.js.map +0 -1
  55. package/lib/types.d.ts +0 -88
  56. package/lib/types.d.ts.map +0 -1
  57. package/lib/types.js +0 -2
  58. package/lib/types.js.map +0 -1
  59. package/src/module/sha256.ts +0 -5
  60. package/src/services/migrateUserDataService.ts +0 -37
  61. package/src/services/updateUserAvatarAfterUploadService.ts +0 -42
  62. package/src/services/userServices.ts +0 -15
  63. package/src/types.ts +0 -101
@@ -1,99 +1,39 @@
1
- import bcrypt from 'bcryptjs';
2
- import { ModuleInput } from '@unchainedshop/mongodb';
3
- import { User, UserQuery, Email, UserLastLogin, UserProfile, UserData } from '../types.js';
4
- import { emit, registerEvents } from '@unchainedshop/events';
1
+ import * as bcrypt from '@node-rs/bcrypt';
5
2
  import {
3
+ ModuleInput,
4
+ Address,
5
+ Contact,
6
6
  generateDbFilterById,
7
7
  buildSortOptions,
8
8
  mongodb,
9
9
  generateDbObjectId,
10
10
  } from '@unchainedshop/mongodb';
11
- import { systemLocale } from '@unchainedshop/utils';
12
- import { FileDirector } from '@unchainedshop/file-upload';
13
- import { SortDirection, SortOption } from '@unchainedshop/utils';
14
- import { UsersCollection } from '../db/UsersCollection.js';
11
+ import {
12
+ User,
13
+ UserQuery,
14
+ Email,
15
+ UserLastLogin,
16
+ UserProfile,
17
+ UsersCollection,
18
+ } from '../db/UsersCollection.js';
19
+ import { emit, registerEvents } from '@unchainedshop/events';
20
+ import { systemLocale, SortDirection, SortOption, sha256 } from '@unchainedshop/utils';
15
21
  import addMigrations from './addMigrations.js';
16
22
  import { userSettings, UserSettingsOptions } from '../users-settings.js';
17
- import { configureUsersWebAuthnModule, UsersWebAuthnModule } from './configureUsersWebAuthnModule.js';
23
+ import { configureUsersWebAuthnModule } from './configureUsersWebAuthnModule.js';
18
24
  import * as pbkdf2 from './pbkdf2.js';
19
- import * as sha256 from './sha256.js';
20
- import type { Address, Contact } from '@unchainedshop/mongodb';
21
- import { UserServices } from '../services/userServices.js';
22
- import { FileServices, FilesModule } from '@unchainedshop/core-files';
23
-
24
- export type UsersModule = {
25
- // Submodules
26
- webAuthn: UsersWebAuthnModule;
27
-
28
- // Queries
29
- count: (query: UserQuery) => Promise<number>;
30
- findUserById: (userId: string) => Promise<User>;
31
- findUserByToken: (hashedToken?: string) => Promise<User>;
32
- findUserByResetToken: (token: string) => Promise<User>;
33
- findUnverifiedEmailToken: (token: string) => Promise<{
34
- userId: string;
35
- address: string;
36
- when: Date;
37
- }>;
38
- findUserByEmail(email: string): Promise<User>;
39
- findUserByUsername(username: string): Promise<User>;
40
- findUser: (
41
- selector: UserQuery & { sort?: Array<SortOption> },
42
- options?: mongodb.FindOptions,
43
- ) => Promise<User>;
44
- findUsers: (
45
- query: UserQuery & {
46
- sort?: Array<SortOption>;
47
- limit?: number;
48
- offset?: number;
49
- },
50
- ) => Promise<Array<User>>;
51
- userExists: (query: { userId: string }) => Promise<boolean>;
52
- // Transformations
53
- primaryEmail: (user: User) => Email;
54
- userLocale: (user: User) => Intl.Locale;
55
-
56
- // Mutations
57
- createUser: (
58
- userData: UserData,
59
- options: { skipMessaging?: boolean; skipPasswordEnrollment?: boolean },
60
- ) => Promise<string>;
61
- addEmail: (userId: string, email: string) => Promise<void>;
62
- removeEmail: (userId: string, email: string) => Promise<void>;
63
- sendVerificationEmail: (userId: string, email: string) => Promise<void>;
64
- sendResetPasswordEmail: (userId: string, email: string, isEnrollment?: boolean) => Promise<void>;
65
- verifyEmail: (userId: string, email: string) => Promise<void>;
66
- setUsername: (userId: string, newUsername: string) => Promise<void>;
67
- setPassword: (userId: string, newPassword?: string) => Promise<void>;
68
- verifyPassword: (hashObject: Record<string, string>, password: string) => Promise<boolean>;
69
- addRoles: (userId: string, roles: Array<string>) => Promise<number>;
70
- updateAvatar: (_id: string, fileId: string) => Promise<User>;
71
- updateGuest: (user: User, guest: boolean) => Promise<void>;
72
- updateHeartbeat: (userId: string, doc: UserLastLogin) => Promise<User>;
73
- updateLastBillingAddress: (_id: string, lastAddress: Address) => Promise<User>;
74
- updateLastContact: (_id: string, lastContact: Contact) => Promise<User>;
75
- updateProfile: (
76
- _id: string,
77
- { profile, meta }: { profile?: UserProfile; meta?: any },
78
- ) => Promise<User>;
79
- delete: (userId: string) => Promise<User>;
80
- updateRoles: (_id: string, roles: Array<string>) => Promise<User>;
81
- updateTags: (_id: string, tags: Array<string>) => Promise<User>;
82
- updateUser: (
83
- selector: mongodb.Filter<User>,
84
- modifier: mongodb.UpdateFilter<User>,
85
- options: mongodb.FindOneAndUpdateOptions,
86
- ) => Promise<User>;
87
- addPushSubscription: (
88
- userId: string,
89
- subscription: any,
90
- options?: {
91
- userAgent: string;
92
- unsubscribeFromOtherUsers: boolean;
93
- },
94
- ) => Promise<void>;
95
- removePushSubscription: (userId: string, p256dh: string) => Promise<void>;
96
- };
25
+
26
+ export interface UserData {
27
+ email?: string;
28
+ guest?: boolean;
29
+ initialPassword?: boolean;
30
+ lastBillingAddress?: User['lastBillingAddress'];
31
+ password: string | null;
32
+ webAuthnPublicKeyCredentials?: any;
33
+ profile?: UserProfile;
34
+ roles?: Array<string>;
35
+ username?: string;
36
+ }
97
37
 
98
38
  const USER_EVENTS = [
99
39
  'USER_ACCOUNT_ACTION',
@@ -118,36 +58,44 @@ export const removeConfidentialServiceHashes = (rawUser: User): User => {
118
58
  return user;
119
59
  };
120
60
 
121
- export const buildFindSelector = ({ includeGuests, queryString, ...rest }: UserQuery) => {
61
+ export const buildFindSelector = ({
62
+ includeGuests,
63
+ queryString,
64
+ emailVerified,
65
+ lastLogin,
66
+ ...rest
67
+ }: UserQuery) => {
122
68
  const selector: mongodb.Filter<User> = { ...rest, deleted: null };
123
69
  if (!includeGuests) selector.guest = { $in: [false, null] };
70
+ if (emailVerified === true) {
71
+ selector['emails.verified'] = true;
72
+ }
73
+ if (emailVerified === false) {
74
+ // We need to use $ne here else we'd also find users with many emails where one is
75
+ // unverified
76
+ selector['emails.verified'] = { $ne: true };
77
+ }
78
+ if (lastLogin?.start) {
79
+ selector['lastLogin.timestamp'] = { $exists: true };
80
+ }
81
+ if (lastLogin?.end) {
82
+ selector['lastLogin.timestamp'].$lte = new Date(lastLogin.end);
83
+ }
84
+ if (lastLogin?.start) {
85
+ selector['lastLogin.timestamp'].$gte = new Date(lastLogin.start);
86
+ }
124
87
  if (queryString) {
125
88
  (selector as any).$text = { $search: queryString };
126
89
  }
127
90
  return selector;
128
91
  };
129
92
 
130
- FileDirector.registerFileUploadCallback<{
131
- services: {
132
- users: UserServices;
133
- files: FileServices;
134
- };
135
- modules: {
136
- users: UsersModule;
137
- files: FilesModule;
138
- };
139
- }>('user-avatars', async (file, context) => {
140
- const { services } = context;
141
- return services.users.updateUserAvatarAfterUpload({ file }, context);
142
- });
143
-
144
93
  export const configureUsersModule = async ({
145
94
  db,
146
95
  options,
147
96
  migrationRepository,
148
97
  }: ModuleInput<UserSettingsOptions>) => {
149
98
  userSettings.configureSettings(options || {}, db);
150
-
151
99
  registerEvents(USER_EVENTS);
152
100
  const Users = await UsersCollection(db);
153
101
 
@@ -185,13 +133,13 @@ export const configureUsersModule = async ({
185
133
  when: Date;
186
134
  }> {
187
135
  if (!plainToken) return null;
188
- const token = await sha256.hash(plainToken);
136
+ const token = await sha256(plainToken);
189
137
  const user = await Users.findOne(
190
138
  {
191
139
  'services.email.verificationTokens': {
192
140
  $elemMatch: {
193
141
  token,
194
- when: { $gt: new Date().getTime() },
142
+ when: { $gt: new Date(new Date().getTime() - 1000 * 60 * 60) },
195
143
  },
196
144
  },
197
145
  },
@@ -206,48 +154,59 @@ export const configureUsersModule = async ({
206
154
  },
207
155
 
208
156
  async verifyEmail(userId: string, address: string): Promise<void> {
209
- await Users.updateOne(
210
- { _id: userId },
157
+ const updated = await Users.updateOne(
158
+ {
159
+ _id: userId,
160
+ emails: { $elemMatch: { address: { $regex: address, $options: 'i' }, verified: false } },
161
+ },
211
162
  {
212
163
  $set: {
213
- 'emails.$[email].verified': true,
164
+ 'emails.$.verified': true,
214
165
  },
215
166
  $pull: {
216
167
  'services.email.verificationTokens': {
217
- address,
168
+ address: { $regex: address, $options: 'i' },
218
169
  },
219
170
  },
220
171
  },
221
- {
222
- arrayFilters: [{ 'email.address': address }],
223
- },
224
172
  );
225
173
 
226
- await emit('USER_ACCOUNT_ACTION', {
227
- action: 'email-verified',
228
- address,
229
- userId,
230
- });
174
+ if (updated.modifiedCount > 0) {
175
+ await emit('USER_ACCOUNT_ACTION', {
176
+ action: 'email-verified',
177
+ address,
178
+ userId,
179
+ });
180
+ }
231
181
  },
232
182
 
233
- async findUserByResetToken(plainToken: string): Promise<User> {
234
- const token = await sha256.hash(plainToken);
183
+ async findResetToken(plainToken: string): Promise<{
184
+ userId: string;
185
+ address: string;
186
+ when: Date;
187
+ }> {
188
+ const token = await sha256(plainToken);
235
189
  const user = await Users.findOne(
236
190
  {
237
191
  'services.password.reset': {
238
192
  $elemMatch: {
239
193
  token,
240
- when: { $gt: new Date().getTime() },
194
+ when: { $gt: new Date(new Date().getTime() - 1000 * 60 * 60) },
241
195
  },
242
196
  },
243
197
  },
244
198
  {},
245
199
  );
246
- return user;
200
+ if (!user) return null;
201
+ const resetToken = user.services.password.reset.find((v) => v.token === token);
202
+ return {
203
+ userId: user._id,
204
+ ...resetToken,
205
+ };
247
206
  },
248
207
 
249
- async findUserByToken(plainToken?: string): Promise<User> {
250
- const token = await sha256.hash(plainToken);
208
+ async findUserByToken(plainToken: string): Promise<User> {
209
+ const token = await sha256(plainToken);
251
210
 
252
211
  if (token) {
253
212
  return Users.findOne({
@@ -276,7 +235,7 @@ export const configureUsersModule = async ({
276
235
  offset?: number;
277
236
  }): Promise<Array<User>> {
278
237
  const defaultSort = [{ key: 'created', value: SortDirection.ASC }] as SortOption[];
279
- const selector = buildFindSelector(query);
238
+ const selector = buildFindSelector({ ...query });
280
239
 
281
240
  if (query.queryString) {
282
241
  return Users.find(selector, {
@@ -295,22 +254,24 @@ export const configureUsersModule = async ({
295
254
  },
296
255
 
297
256
  async userExists({ userId }: { userId: string }): Promise<boolean> {
298
- const selector = generateDbFilterById<User>(userId);
299
- selector.deleted = null; // skip deleted users when checked for existance!
300
- const userCount = await Users.countDocuments(selector, { limit: 1 });
257
+ const userCount = await Users.countDocuments({ _id: userId, deleted: null }, { limit: 1 });
301
258
  return userCount === 1;
302
259
  },
303
260
 
304
261
  // Transformations
305
262
  primaryEmail(user: User): Email {
306
- return (user.emails || []).sort(
263
+ return (user.emails || []).toSorted(
307
264
  (left, right) => Number(right.verified) - Number(left.verified),
308
265
  )?.[0];
309
266
  },
310
267
 
311
268
  userLocale(user: User): Intl.Locale {
312
269
  if (!user?.lastLogin?.locale) return systemLocale;
313
- return new Intl.Locale(user.lastLogin.locale);
270
+ try {
271
+ return new Intl.Locale(user.lastLogin.locale);
272
+ } catch {
273
+ return systemLocale;
274
+ }
314
275
  },
315
276
 
316
277
  // Mutations
@@ -359,6 +320,7 @@ export const configureUsersModule = async ({
359
320
  _id: normalizedUserData._id || generateDbObjectId(),
360
321
  roles: roles || [],
361
322
  initialPassword: Boolean(initialPassword),
323
+ services,
362
324
  emails: email ? [{ address: email, verified: false }] : [],
363
325
  created: new Date(),
364
326
  };
@@ -398,7 +360,9 @@ export const configureUsersModule = async ({
398
360
  return userId;
399
361
  },
400
362
 
401
- async hashPassword(password) {
363
+ async hashPassword(password: string): Promise<{
364
+ pbkdf2: string;
365
+ }> {
402
366
  const salt = pbkdf2.generateSalt();
403
367
  const hashedPassword = await pbkdf2.getDerivedKey(salt, password);
404
368
  return { pbkdf2: `${salt}:${hashedPassword}` };
@@ -409,7 +373,7 @@ export const configureUsersModule = async ({
409
373
  plainPassword: string,
410
374
  ): Promise<boolean> {
411
375
  if (bcryptHash) {
412
- const password = await sha256.hash(plainPassword);
376
+ const password = await sha256(plainPassword);
413
377
  return bcrypt.compare(password, bcryptHash);
414
378
  }
415
379
  if (pbkdf2SaltAndHash) {
@@ -450,9 +414,9 @@ export const configureUsersModule = async ({
450
414
  async sendResetPasswordEmail(userId: string, email: string, isEnrollment?: boolean): Promise<void> {
451
415
  const plainToken = crypto.randomUUID();
452
416
  const resetToken = {
453
- token: await sha256.hash(plainToken),
417
+ token: await sha256(plainToken),
454
418
  address: email,
455
- when: new Date().getTime() + 1000 * 60 * 60, // 1 hour
419
+ when: new Date(),
456
420
  };
457
421
 
458
422
  await Users.updateOne(
@@ -475,9 +439,9 @@ export const configureUsersModule = async ({
475
439
  async sendVerificationEmail(userId: string, email: string): Promise<void> {
476
440
  const plainToken = crypto.randomUUID();
477
441
  const verificationToken = {
478
- token: await sha256.hash(plainToken),
442
+ token: await sha256(plainToken),
479
443
  address: email,
480
- when: new Date().getTime() + 1000 * 60 * 60, // 1 hour
444
+ when: new Date(),
481
445
  };
482
446
 
483
447
  await Users.updateOne(
@@ -529,12 +493,12 @@ export const configureUsersModule = async ({
529
493
  });
530
494
  },
531
495
 
532
- async setPassword(userId: string, plainPassword: string) {
496
+ async setPassword(userId: string, plainPassword: string): Promise<User> {
533
497
  if (!(await userSettings.validatePassword(plainPassword))) {
534
498
  throw new Error(`Password ***** is invalid`, { cause: 'PASSWORD_INVALID' });
535
499
  }
536
500
  const password = plainPassword || crypto.randomUUID().split('-').pop();
537
- await Users.updateOne(
501
+ const user = await Users.findOneAndUpdate(
538
502
  { _id: userId },
539
503
  {
540
504
  $set: {
@@ -544,11 +508,28 @@ export const configureUsersModule = async ({
544
508
  ),
545
509
  },
546
510
  },
511
+ { returnDocument: 'after' },
547
512
  );
548
- const user = await Users.findOne({ _id: userId }, {});
549
513
  await emit('USER_UPDATE_PASSWORD', {
550
514
  user: removeConfidentialServiceHashes(user),
551
515
  });
516
+ return user;
517
+ },
518
+
519
+ async resetPassword(token: string, newPassword: string): Promise<User> {
520
+ const resetToken = await this.findResetToken(token);
521
+ if (!resetToken) return null;
522
+ const updatedUser = await this.setPassword(resetToken.userId, newPassword);
523
+ if (updatedUser) {
524
+ // Now invalidate the reset token
525
+ await emit('USER_ACCOUNT_ACTION', {
526
+ action: 'password-resetted',
527
+ userId: updatedUser._id,
528
+ });
529
+
530
+ await this.verifyEmail(updatedUser._id, resetToken.address);
531
+ }
532
+ return updatedUser;
552
533
  },
553
534
 
554
535
  updateAvatar: async (_id: string, fileId: string): Promise<User> => {
@@ -601,42 +582,38 @@ export const configureUsersModule = async ({
601
582
  return user;
602
583
  },
603
584
 
604
- delete: async (userId: string): Promise<User> => {
605
- const userFilter = generateDbFilterById(userId);
606
-
607
- const existingUser = await Users.findOne(userFilter, {
608
- projection: { emails: true, username: true },
609
- });
610
- if (!existingUser) return null;
611
-
612
- const uuid = crypto.randomUUID();
613
- const obfuscatedEmails = existingUser.emails?.flatMap(({ address, verified }) => {
614
- if (!verified) return [];
615
- return [
616
- {
617
- address: `${address}@${uuid}.unchained.local`,
618
- verified: true,
585
+ markDeleted: async (userId: string): Promise<User> => {
586
+ const user = await Users.findOneAndUpdate(
587
+ { _id: userId },
588
+ {
589
+ $set: {
590
+ username: `deleted-${Date.now()}`,
591
+ deleted: new Date(),
592
+ emails: [],
593
+ roles: [],
594
+ profile: null,
595
+ lastBillingAddress: null,
596
+ services: {},
597
+ pushSubscriptions: [],
598
+ avatarId: null,
599
+ initialPassword: false,
600
+ lastContact: null,
601
+ lastLogin: null,
619
602
  },
620
- ];
621
- });
622
-
623
- const obfuscatedUsername = existingUser.username ? `${existingUser.username}-${uuid}` : null;
624
-
625
- Users.updateOne(userFilter, {
626
- $set: {
627
- emails: obfuscatedEmails,
628
- username: obfuscatedUsername,
629
- services: {},
630
603
  },
631
- });
604
+ { returnDocument: 'after' },
605
+ );
632
606
 
633
- const user = await Users.findOneAndDelete(userFilter);
634
607
  await emit('USER_REMOVE', {
635
- user: removeConfidentialServiceHashes(user),
608
+ user,
636
609
  });
637
610
  return user;
638
611
  },
639
612
 
613
+ deletePermanently: async ({ userId }: { userId: string }): Promise<User> => {
614
+ return Users.findOneAndDelete({ _id: userId });
615
+ },
616
+
640
617
  updateProfile: async (
641
618
  userId: string,
642
619
  updatedData: { profile?: UserProfile; meta?: any },
@@ -679,6 +656,8 @@ export const configureUsersModule = async ({
679
656
  const userFilter = generateDbFilterById(_id);
680
657
  const user = await Users.findOne(userFilter, {});
681
658
 
659
+ if (!lastBillingAddress) return user;
660
+
682
661
  const modifier = {
683
662
  $set: {
684
663
  lastBillingAddress,
@@ -701,9 +680,12 @@ export const configureUsersModule = async ({
701
680
  returnDocument: 'after',
702
681
  });
703
682
 
704
- await emit('USER_UPDATE_BILLING_ADDRESS', {
705
- user: removeConfidentialServiceHashes(user),
706
- });
683
+ if (updatedUser) {
684
+ await emit('USER_UPDATE_BILLING_ADDRESS', {
685
+ user: removeConfidentialServiceHashes(updatedUser),
686
+ });
687
+ }
688
+
707
689
  return updatedUser;
708
690
  },
709
691
 
@@ -829,3 +811,5 @@ export const configureUsersModule = async ({
829
811
  },
830
812
  };
831
813
  };
814
+
815
+ export type UsersModule = Awaited<ReturnType<typeof configureUsersModule>>;
@@ -1,30 +1,8 @@
1
- /// <reference lib="dom" />
2
1
  import { ModuleInput } from '@unchainedshop/mongodb';
3
- import {
4
- Fido2Lib,
5
- PublicKeyCredentialCreationOptions,
6
- PublicKeyCredentialRequestOptions,
7
- } from 'fido2-lib';
8
2
  import { createLogger } from '@unchainedshop/logger';
9
3
  import { WebAuthnCredentialsCreationRequestsCollection } from '../db/WebAuthnCredentialsCreationRequestsCollection.js';
10
4
 
11
- export interface UsersWebAuthnModule {
12
- findMDSMetadataForAAGUID: (aaguid: string) => Promise<any>;
13
-
14
- createCredentialCreationOptions: (
15
- origin: string,
16
- username: string,
17
- extensionOptions?: any,
18
- ) => Promise<any>;
19
- verifyCredentialCreation: (username: string, credentials: any) => Promise<any>;
20
-
21
- createCredentialRequestOptions: (
22
- origin: string,
23
- username?: string,
24
- extensionOptions?: any,
25
- ) => Promise<any>;
26
- verifyCredentialRequest: (userPublicKeys: any[], username: string, credentials: any) => Promise<any>;
27
- }
5
+ import type { PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOptions } from 'fido2-lib';
28
6
 
29
7
  const logger = createLogger('unchained:core-users');
30
8
 
@@ -40,6 +18,9 @@ const setupMDSCollection = async () => {
40
18
  try {
41
19
  const tocResult = await fetch('https://mds.fidoalliance.org');
42
20
  const tocBase64 = await tocResult.text();
21
+ // eslint-disable-next-line
22
+ // @ts-ignore
23
+ const { Fido2Lib } = await import('fido2-lib');
43
24
  const mc = (Fido2Lib as any).createMdsCollection('FIDO MDS v3'); // createMdsCollection exists but not typed in official package!
44
25
  const tocObj = await mc.addToc(tocBase64);
45
26
  return tocObj.entries;
@@ -66,24 +47,30 @@ export function buf2hex(buffer) {
66
47
  .join('');
67
48
  }
68
49
 
69
- export const configureUsersWebAuthnModule = async ({
70
- db,
71
- }: ModuleInput<any>): Promise<UsersWebAuthnModule> => {
72
- const WebAuthnCredentialsCreationRequests = await WebAuthnCredentialsCreationRequestsCollection(db);
73
-
74
- const thisDomain = new URL(ROOT_URL).hostname;
75
-
76
- const f2l = new Fido2Lib({
77
- rpId: thisDomain,
78
- rpName: EMAIL_WEBSITE_NAME,
79
- rpIcon:
80
- 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAARCSURBVHgB7Ve9UxNbFL8fm7yA4ktegHnlMk98dMbOMnR2D6pnGTqtCH+BWFoBpZXQaaelFbGzEztHHF26DKizzijDx+Zef+fu3vXuJgsxztjonQnZOZw993d+5zOM/epHslFO1a/KS/UbuvTnETsKQ/YDh3+XNi72yuVlzVgbL1YhCaOTk2ssDAI24hHDKsrpfxZkufwCj6t0OUAEBMkrlR6wHzjDMVCd872yemeete7gczeKoh2wQTICsyi0DrTWVc55eHqwu8OGPEMCMNSby5hS89H7Nx0Se9NXVvF1J69O7MDwJsKzgfCcmSP9SQhvxcV6S5cnummCHYVH4kJ9DE9NfHx1+HGLxKo0sSOkvIXHSnyz7mjOySufdLmUN3Vp4slZiSoyXk7NrhHVQui1fGzhzTq+QsZ5U07NtowQ3oH2FavDhXjS2389w7W+lrDgI2+2yfaZAEpT/y5LopjztnEkf1F6GXsc38TvWKO9g91NfO3EBGgjpxzooTosCISvXQjAm7zc1Fyvm7IChRHQM+uVcxGBAbstI84bVcqyUE3lAMyVWkrky0UsuGUYRqeni4zQk1cAk15ELxMYy07OqElKqo5ieVWWymsDAbhKGa9Qatag8P5omcQiUEot9nmb1c9cFkm5FJPJWqWp2UYegKkCMf7XHjRaeGwgs+9T1iPTA8ibkM/h5ev4XwVJthEd7D7kF+ozANRI9B9RlpO+lUO/IcZqz0jGPr8PUUFU7k185mwFZUIwBAuGUqWUob8nxCqLQ0H6qbeOnNmQGftOBVHO9QEw+ozZRKLE8y0wxDxFLISIE6n7KsDfjUS8kBoludZbCYBvl8XNaCMPLAOASsde5vYAlFPbeqWV+q/Pq5xR6HRS6zkWzPzIlXdmGKWXFaM/V06zwAGQaVppSJ3yzk7DAqpcb7UQD/q8cuRoxb75dtlxm5Zb3mzALEj7O7I/zWRUBRurHcO7G9Sw5MXJT+rLh+dmRlSqe6iOm1aOC/5nBELr2yTDOw3YO4Z+x3icq7iB01BMXm4j4dbIO+rtVi6nr7xLBg0tIjN20mGGbBPdVs+8J07mvcjzkbnbGf14tNNeYcb4wJVMH358jpo2zSf1lgCM1V4m6CsFXiUG9Iref9txe4kQXgVT9jqXejNZaEKAvFu4D5ikStBjus3bJcPxdjALNE8OduetHep+yIsXGQcZf9wTxyusGwSFS2mCvmaMcn6Lj9d9OV77BGN7iOuCYQFeqcMPT43+peln2IraFH/EdsvuALDTTTthvE0tIRnvsc/x/8/diIq2HnsSFoKEhXUAWM6zYA5VwoDtaLiV7O85XypFy+hVnbRlhCXoo9zZHWkpGWY3/L613D25PZFzeVVxvZqs6ywSYKZ7/ro+2g8TOu6eSBXAGfWIio2z2n871GY8OgN06BcSfitQuZr1CxuQ3Zh/6qkk0/P3GeV8BeBCYUHc/WHIAAAAAElFTkSuQmCC',
81
- challengeSize: 128,
82
- attestation: 'none',
83
- });
50
+ const initF2L = async () => {
51
+ try {
52
+ const thisDomain = new URL(ROOT_URL).hostname;
53
+ const { Fido2Lib } = await import('fido2-lib');
54
+ const f2l = new Fido2Lib({
55
+ rpId: thisDomain,
56
+ rpName: EMAIL_WEBSITE_NAME,
57
+ rpIcon:
58
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAARCSURBVHgB7Ve9UxNbFL8fm7yA4ktegHnlMk98dMbOMnR2D6pnGTqtCH+BWFoBpZXQaaelFbGzEztHHF26DKizzijDx+Zef+fu3vXuJgsxztjonQnZOZw993d+5zOM/epHslFO1a/KS/UbuvTnETsKQ/YDh3+XNi72yuVlzVgbL1YhCaOTk2ssDAI24hHDKsrpfxZkufwCj6t0OUAEBMkrlR6wHzjDMVCd872yemeete7gczeKoh2wQTICsyi0DrTWVc55eHqwu8OGPEMCMNSby5hS89H7Nx0Se9NXVvF1J69O7MDwJsKzgfCcmSP9SQhvxcV6S5cnummCHYVH4kJ9DE9NfHx1+HGLxKo0sSOkvIXHSnyz7mjOySufdLmUN3Vp4slZiSoyXk7NrhHVQui1fGzhzTq+QsZ5U07NtowQ3oH2FavDhXjS2389w7W+lrDgI2+2yfaZAEpT/y5LopjztnEkf1F6GXsc38TvWKO9g91NfO3EBGgjpxzooTosCISvXQjAm7zc1Fyvm7IChRHQM+uVcxGBAbstI84bVcqyUE3lAMyVWkrky0UsuGUYRqeni4zQk1cAk15ELxMYy07OqElKqo5ieVWWymsDAbhKGa9Qatag8P5omcQiUEot9nmb1c9cFkm5FJPJWqWp2UYegKkCMf7XHjRaeGwgs+9T1iPTA8ibkM/h5ev4XwVJthEd7D7kF+ozANRI9B9RlpO+lUO/IcZqz0jGPr8PUUFU7k185mwFZUIwBAuGUqWUob8nxCqLQ0H6qbeOnNmQGftOBVHO9QEw+ozZRKLE8y0wxDxFLISIE6n7KsDfjUS8kBoludZbCYBvl8XNaCMPLAOASsde5vYAlFPbeqWV+q/Pq5xR6HRS6zkWzPzIlXdmGKWXFaM/V06zwAGQaVppSJ3yzk7DAqpcb7UQD/q8cuRoxb75dtlxm5Zb3mzALEj7O7I/zWRUBRurHcO7G9Sw5MXJT+rLh+dmRlSqe6iOm1aOC/5nBELr2yTDOw3YO4Z+x3icq7iB01BMXm4j4dbIO+rtVi6nr7xLBg0tIjN20mGGbBPdVs+8J07mvcjzkbnbGf14tNNeYcb4wJVMH358jpo2zSf1lgCM1V4m6CsFXiUG9Iref9txe4kQXgVT9jqXejNZaEKAvFu4D5ikStBjus3bJcPxdjALNE8OduetHep+yIsXGQcZf9wTxyusGwSFS2mCvmaMcn6Lj9d9OV77BGN7iOuCYQFeqcMPT43+peln2IraFH/EdsvuALDTTTthvE0tIRnvsc/x/8/diIq2HnsSFoKEhXUAWM6zYA5VwoDtaLiV7O85XypFy+hVnbRlhCXoo9zZHWkpGWY3/L613D25PZFzeVVxvZqs6ywSYKZ7/ro+2g8TOu6eSBXAGfWIio2z2n871GY8OgN06BcSfitQuZr1CxuQ3Zh/6qkk0/P3GeV8BeBCYUHc/WHIAAAAAElFTkSuQmCC',
59
+ challengeSize: 128,
60
+ attestation: 'none',
61
+ });
62
+ return f2l;
63
+ } catch {
64
+ logger.warn(`optional peer npm package 'fido2-lib' not installed, WebAuthn will not work`);
65
+ return null;
66
+ }
67
+ };
84
68
 
69
+ export const configureUsersWebAuthnModule = async ({ db }: ModuleInput<any>) => {
70
+ const WebAuthnCredentialsCreationRequests = await WebAuthnCredentialsCreationRequestsCollection(db);
71
+ const f2l = await initF2L();
85
72
  return {
86
- findMDSMetadataForAAGUID: async (aaguid) => {
73
+ findMDSMetadataForAAGUID: async (aaguid: string) => {
87
74
  const mdsCollection = await fetchMDS();
88
75
  const foundEntry = mdsCollection.find((entry) => {
89
76
  return entry.aaguid === aaguid;
@@ -91,7 +78,13 @@ export const configureUsersWebAuthnModule = async ({
91
78
  return foundEntry?.metadataStatement;
92
79
  },
93
80
 
94
- createCredentialCreationOptions: async (origin, username, extensionOptions) => {
81
+ createCredentialCreationOptions: async (
82
+ origin: string,
83
+ username: string,
84
+ extensionOptions?: any,
85
+ ) => {
86
+ if (!f2l) return null;
87
+
95
88
  const registrationOptions = await f2l.attestationOptions(extensionOptions);
96
89
  const challenge = Buffer.from(registrationOptions.challenge).toString('base64');
97
90
  const { insertedId } = await WebAuthnCredentialsCreationRequests.insertOne({
@@ -109,7 +102,13 @@ export const configureUsersWebAuthnModule = async ({
109
102
  } as SerializedOptions<PublicKeyCredentialCreationOptions>;
110
103
  },
111
104
 
112
- createCredentialRequestOptions: async (origin, username, extensionOptions) => {
105
+ createCredentialRequestOptions: async (
106
+ origin: string,
107
+ username?: string,
108
+ extensionOptions?: any,
109
+ ) => {
110
+ if (!f2l) return null;
111
+
113
112
  const loginOptions = await f2l.assertionOptions(extensionOptions);
114
113
  const challenge = Buffer.from(loginOptions.challenge).toString('base64');
115
114
  const { insertedId } = await WebAuthnCredentialsCreationRequests.insertOne({
@@ -127,7 +126,9 @@ export const configureUsersWebAuthnModule = async ({
127
126
  } as SerializedOptions<PublicKeyCredentialRequestOptions>;
128
127
  },
129
128
 
130
- verifyCredentialCreation: async (username, credentials) => {
129
+ verifyCredentialCreation: async (username: string, credentials: any) => {
130
+ if (!f2l) return null;
131
+
131
132
  const request = await WebAuthnCredentialsCreationRequests.findOne(
132
133
  {
133
134
  username,
@@ -174,7 +175,9 @@ export const configureUsersWebAuthnModule = async ({
174
175
  return { publicKey, counter, id: credentials.id, aaguid, created: new Date() };
175
176
  },
176
177
 
177
- verifyCredentialRequest: async (userPublicKeys, username, credentials) => {
178
+ verifyCredentialRequest: async (userPublicKeys: any[], username: string, credentials: any) => {
179
+ if (!f2l) return null;
180
+
178
181
  const request = await WebAuthnCredentialsCreationRequests.findOne(
179
182
  {
180
183
  _id: credentials.requestId,
@@ -193,7 +196,7 @@ export const configureUsersWebAuthnModule = async ({
193
196
  return credentials.id === publicCredentials.id;
194
197
  }) || {};
195
198
 
196
- if (!publicKey) throw new Error('WebAuthn not setup');
199
+ if (!publicKey) return null;
197
200
 
198
201
  const assertionExpectations = {
199
202
  challenge: request.challenge,
@@ -4,7 +4,7 @@ const PBKDF2_KEY_LENGTH = 256; // Bytes
4
4
  const PBKDF2_SALT_LENGTH = 16; // Bytes
5
5
 
6
6
  export function generateSalt(saltLength = PBKDF2_SALT_LENGTH) {
7
- const array = new Uint32Array(saltLength);
7
+ const array = new Uint8Array(saltLength);
8
8
  return Buffer.from(crypto.getRandomValues(array)).toString('hex');
9
9
  }
10
10
 
@@ -1,6 +1,6 @@
1
- import { User } from '../types.js';
2
1
  import { removeConfidentialServiceHashes } from './configureUsersModule.js';
3
2
  import user from '../../tests/mock/user-mock.js';
3
+ import { User } from '../db/UsersCollection.js';
4
4
 
5
5
  describe('removeConfidentialServiceHashes', () => {
6
6
  it('Should remove sensitive user credentials ', () => {