@unchainedshop/core-users 3.0.0-alpha7 → 3.0.0-rc10

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 +105 -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 +152 -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
@@ -399,7 +360,9 @@ export const configureUsersModule = async ({
399
360
  return userId;
400
361
  },
401
362
 
402
- async hashPassword(password) {
363
+ async hashPassword(password: string): Promise<{
364
+ pbkdf2: string;
365
+ }> {
403
366
  const salt = pbkdf2.generateSalt();
404
367
  const hashedPassword = await pbkdf2.getDerivedKey(salt, password);
405
368
  return { pbkdf2: `${salt}:${hashedPassword}` };
@@ -410,7 +373,7 @@ export const configureUsersModule = async ({
410
373
  plainPassword: string,
411
374
  ): Promise<boolean> {
412
375
  if (bcryptHash) {
413
- const password = await sha256.hash(plainPassword);
376
+ const password = await sha256(plainPassword);
414
377
  return bcrypt.compare(password, bcryptHash);
415
378
  }
416
379
  if (pbkdf2SaltAndHash) {
@@ -451,9 +414,9 @@ export const configureUsersModule = async ({
451
414
  async sendResetPasswordEmail(userId: string, email: string, isEnrollment?: boolean): Promise<void> {
452
415
  const plainToken = crypto.randomUUID();
453
416
  const resetToken = {
454
- token: await sha256.hash(plainToken),
417
+ token: await sha256(plainToken),
455
418
  address: email,
456
- when: new Date().getTime() + 1000 * 60 * 60, // 1 hour
419
+ when: new Date(),
457
420
  };
458
421
 
459
422
  await Users.updateOne(
@@ -476,9 +439,9 @@ export const configureUsersModule = async ({
476
439
  async sendVerificationEmail(userId: string, email: string): Promise<void> {
477
440
  const plainToken = crypto.randomUUID();
478
441
  const verificationToken = {
479
- token: await sha256.hash(plainToken),
442
+ token: await sha256(plainToken),
480
443
  address: email,
481
- when: new Date().getTime() + 1000 * 60 * 60, // 1 hour
444
+ when: new Date(),
482
445
  };
483
446
 
484
447
  await Users.updateOne(
@@ -530,12 +493,12 @@ export const configureUsersModule = async ({
530
493
  });
531
494
  },
532
495
 
533
- async setPassword(userId: string, plainPassword: string) {
496
+ async setPassword(userId: string, plainPassword: string): Promise<User> {
534
497
  if (!(await userSettings.validatePassword(plainPassword))) {
535
498
  throw new Error(`Password ***** is invalid`, { cause: 'PASSWORD_INVALID' });
536
499
  }
537
500
  const password = plainPassword || crypto.randomUUID().split('-').pop();
538
- await Users.updateOne(
501
+ const user = await Users.findOneAndUpdate(
539
502
  { _id: userId },
540
503
  {
541
504
  $set: {
@@ -545,11 +508,28 @@ export const configureUsersModule = async ({
545
508
  ),
546
509
  },
547
510
  },
511
+ { returnDocument: 'after' },
548
512
  );
549
- const user = await Users.findOne({ _id: userId }, {});
550
513
  await emit('USER_UPDATE_PASSWORD', {
551
514
  user: removeConfidentialServiceHashes(user),
552
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;
553
533
  },
554
534
 
555
535
  updateAvatar: async (_id: string, fileId: string): Promise<User> => {
@@ -602,42 +582,38 @@ export const configureUsersModule = async ({
602
582
  return user;
603
583
  },
604
584
 
605
- delete: async (userId: string): Promise<User> => {
606
- const userFilter = generateDbFilterById(userId);
607
-
608
- const existingUser = await Users.findOne(userFilter, {
609
- projection: { emails: true, username: true },
610
- });
611
- if (!existingUser) return null;
612
-
613
- const uuid = crypto.randomUUID();
614
- const obfuscatedEmails = existingUser.emails?.flatMap(({ address, verified }) => {
615
- if (!verified) return [];
616
- return [
617
- {
618
- address: `${address}@${uuid}.unchained.local`,
619
- 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,
620
602
  },
621
- ];
622
- });
623
-
624
- const obfuscatedUsername = existingUser.username ? `${existingUser.username}-${uuid}` : null;
625
-
626
- Users.updateOne(userFilter, {
627
- $set: {
628
- emails: obfuscatedEmails,
629
- username: obfuscatedUsername,
630
- services: {},
631
603
  },
632
- });
604
+ { returnDocument: 'after' },
605
+ );
633
606
 
634
- const user = await Users.findOneAndDelete(userFilter);
635
607
  await emit('USER_REMOVE', {
636
- user: removeConfidentialServiceHashes(user),
608
+ user,
637
609
  });
638
610
  return user;
639
611
  },
640
612
 
613
+ deletePermanently: async ({ userId }: { userId: string }): Promise<User> => {
614
+ return Users.findOneAndDelete({ _id: userId });
615
+ },
616
+
641
617
  updateProfile: async (
642
618
  userId: string,
643
619
  updatedData: { profile?: UserProfile; meta?: any },
@@ -680,6 +656,8 @@ export const configureUsersModule = async ({
680
656
  const userFilter = generateDbFilterById(_id);
681
657
  const user = await Users.findOne(userFilter, {});
682
658
 
659
+ if (!lastBillingAddress) return user;
660
+
683
661
  const modifier = {
684
662
  $set: {
685
663
  lastBillingAddress,
@@ -702,9 +680,12 @@ export const configureUsersModule = async ({
702
680
  returnDocument: 'after',
703
681
  });
704
682
 
705
- await emit('USER_UPDATE_BILLING_ADDRESS', {
706
- user: removeConfidentialServiceHashes(user),
707
- });
683
+ if (updatedUser) {
684
+ await emit('USER_UPDATE_BILLING_ADDRESS', {
685
+ user: removeConfidentialServiceHashes(updatedUser),
686
+ });
687
+ }
688
+
708
689
  return updatedUser;
709
690
  },
710
691
 
@@ -830,3 +811,5 @@ export const configureUsersModule = async ({
830
811
  },
831
812
  };
832
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 ', () => {
@@ -1,4 +1,5 @@
1
- export * from './types.js';
1
+ export * from './db/UsersCollection.js';
2
+ export * from './db/WebAuthnCredentialsCreationRequestsCollection.js';
2
3
  export * from './module/configureUsersModule.js';
3
- export * from './services/userServices.js';
4
+ export * from './module/configureUsersWebAuthnModule.js';
4
5
  export * from './users-settings.js';