@unchainedshop/core-users 2.12.2 → 3.0.0-alpha2

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 (51) hide show
  1. package/lib/db/UsersCollection.d.ts.map +1 -1
  2. package/lib/db/UsersCollection.js +0 -8
  3. package/lib/db/UsersCollection.js.map +1 -1
  4. package/lib/db/WebAuthnCredentialsCreationRequestsCollection.d.ts +8 -0
  5. package/lib/db/WebAuthnCredentialsCreationRequestsCollection.d.ts.map +1 -0
  6. package/lib/db/WebAuthnCredentialsCreationRequestsCollection.js +13 -0
  7. package/lib/db/WebAuthnCredentialsCreationRequestsCollection.js.map +1 -0
  8. package/lib/module/configureUsersModule.d.ts +88 -2
  9. package/lib/module/configureUsersModule.d.ts.map +1 -1
  10. package/lib/module/configureUsersModule.js +277 -47
  11. package/lib/module/configureUsersModule.js.map +1 -1
  12. package/lib/module/configureUsersWebAuthnModule.d.ts +25 -0
  13. package/lib/module/configureUsersWebAuthnModule.d.ts.map +1 -0
  14. package/lib/module/configureUsersWebAuthnModule.js +148 -0
  15. package/lib/module/configureUsersWebAuthnModule.js.map +1 -0
  16. package/lib/module/pbkdf2.d.ts +4 -0
  17. package/lib/module/pbkdf2.d.ts.map +1 -0
  18. package/lib/module/pbkdf2.js +27 -0
  19. package/lib/module/pbkdf2.js.map +1 -0
  20. package/lib/module/sha256.d.ts +2 -0
  21. package/lib/module/sha256.d.ts.map +1 -0
  22. package/lib/module/sha256.js +6 -0
  23. package/lib/module/sha256.js.map +1 -0
  24. package/lib/services/migrateUserDataService.d.ts +3 -0
  25. package/lib/services/migrateUserDataService.d.ts.map +1 -0
  26. package/lib/services/migrateUserDataService.js +18 -0
  27. package/lib/services/migrateUserDataService.js.map +1 -0
  28. package/lib/services/userServices.d.ts +6 -2
  29. package/lib/services/userServices.d.ts.map +1 -1
  30. package/lib/services/userServices.js +2 -0
  31. package/lib/services/userServices.js.map +1 -1
  32. package/lib/users-index.d.ts +1 -0
  33. package/lib/users-index.d.ts.map +1 -1
  34. package/lib/users-index.js +1 -0
  35. package/lib/users-index.js.map +1 -1
  36. package/lib/users-settings.d.ts +3 -0
  37. package/lib/users-settings.d.ts.map +1 -0
  38. package/lib/users-settings.js +51 -0
  39. package/lib/users-settings.js.map +1 -0
  40. package/package.json +12 -10
  41. package/src/db/UsersCollection.ts +0 -9
  42. package/src/db/WebAuthnCredentialsCreationRequestsCollection.ts +20 -0
  43. package/src/module/configureUsersModule.ts +411 -62
  44. package/src/module/configureUsersWebAuthnModule.ts +201 -0
  45. package/src/module/pbkdf2.ts +39 -0
  46. package/src/module/sha256.ts +5 -0
  47. package/src/services/migrateUserDataService.ts +31 -0
  48. package/src/services/userServices.ts +3 -2
  49. package/src/users-index.ts +1 -0
  50. package/src/users-settings.ts +71 -0
  51. package/tests/mock/user-mock.ts +0 -8
@@ -0,0 +1,201 @@
1
+ /// <reference lib="dom" />
2
+ import { ModuleInput } from '@unchainedshop/types/core.js';
3
+ import {
4
+ Fido2Lib,
5
+ PublicKeyCredentialCreationOptions,
6
+ PublicKeyCredentialRequestOptions,
7
+ } from 'fido2-lib';
8
+ import { createLogger } from '@unchainedshop/logger';
9
+ import { WebAuthnCredentialsCreationRequestsCollection } from '../db/WebAuthnCredentialsCreationRequestsCollection.js';
10
+
11
+ const logger = createLogger('unchained:core-users');
12
+
13
+ const { ROOT_URL, EMAIL_WEBSITE_NAME } = process.env;
14
+
15
+ type SerializedOptions<T> = Omit<T, 'challenge' | 'requestId'> & {
16
+ challenge: string;
17
+ requestId: number;
18
+ };
19
+
20
+ let setupMDSPromise;
21
+ const setupMDSCollection = async () => {
22
+ try {
23
+ const tocResult = await fetch('https://mds.fidoalliance.org');
24
+ const tocBase64 = await tocResult.text();
25
+ const mc = (Fido2Lib as any).createMdsCollection('FIDO MDS v3'); // createMdsCollection exists but not typed in official package!
26
+ const tocObj = await mc.addToc(tocBase64);
27
+ return tocObj.entries;
28
+ } catch (e) {
29
+ logger.error(e);
30
+ return [];
31
+ }
32
+ };
33
+
34
+ const fetchMDS = async () => {
35
+ if (setupMDSPromise) return setupMDSPromise;
36
+ setupMDSPromise = setupMDSCollection();
37
+ return setupMDSPromise;
38
+ };
39
+
40
+ export function toArrayBuffer(buffer) {
41
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
42
+ }
43
+
44
+ export function buf2hex(buffer) {
45
+ // buffer is an ArrayBuffer
46
+ return Array.prototype.map
47
+ .call(new Uint8Array(buffer), (x) => `00${x.toString(16)}`.slice(-2))
48
+ .join('');
49
+ }
50
+
51
+ export const configureUsersWebAuthnModule = async ({ db }: ModuleInput<any>) => {
52
+ const WebAuthnCredentialsCreationRequests = await WebAuthnCredentialsCreationRequestsCollection(db);
53
+
54
+ const thisDomain = new URL(ROOT_URL).hostname;
55
+
56
+ const f2l = new Fido2Lib({
57
+ rpId: thisDomain,
58
+ rpName: EMAIL_WEBSITE_NAME,
59
+ rpIcon:
60
+ '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',
61
+ challengeSize: 128,
62
+ attestation: 'none',
63
+ });
64
+
65
+ return {
66
+ findMDSMetadataForAAGUID: async (aaguid) => {
67
+ const mdsCollection = await fetchMDS();
68
+ const foundEntry = mdsCollection.find((entry) => {
69
+ return entry.aaguid === aaguid;
70
+ });
71
+ return foundEntry?.metadataStatement;
72
+ },
73
+
74
+ createCredentialCreationOptions: async (origin, username, extensionOptions) => {
75
+ const registrationOptions = await f2l.attestationOptions(extensionOptions);
76
+ const challenge = Buffer.from(registrationOptions.challenge).toString('base64');
77
+ const { insertedId } = await WebAuthnCredentialsCreationRequests.insertOne({
78
+ _id: new Date().getTime(),
79
+ challenge,
80
+ origin,
81
+ factor: (registrationOptions as any).factor || 'either',
82
+ username,
83
+ });
84
+
85
+ return {
86
+ ...registrationOptions,
87
+ challenge,
88
+ requestId: insertedId,
89
+ } as SerializedOptions<PublicKeyCredentialCreationOptions>;
90
+ },
91
+
92
+ createCredentialRequestOptions: async (origin, username, extensionOptions) => {
93
+ const loginOptions = await f2l.assertionOptions(extensionOptions);
94
+ const challenge = Buffer.from(loginOptions.challenge).toString('base64');
95
+ const { insertedId } = await WebAuthnCredentialsCreationRequests.insertOne({
96
+ _id: new Date().getTime(),
97
+ challenge,
98
+ origin,
99
+ factor: (loginOptions as any).factor || 'either',
100
+ username,
101
+ });
102
+
103
+ return {
104
+ ...loginOptions,
105
+ challenge,
106
+ requestId: insertedId,
107
+ } as SerializedOptions<PublicKeyCredentialRequestOptions>;
108
+ },
109
+
110
+ verifyCredentialCreation: async (username, credentials) => {
111
+ const request = await WebAuthnCredentialsCreationRequests.findOne(
112
+ {
113
+ username,
114
+ },
115
+ { sort: { _id: -1 } },
116
+ );
117
+
118
+ const attestationExpectations = {
119
+ challenge: request.challenge,
120
+ origin: request.origin,
121
+ factor: request.factor,
122
+ };
123
+
124
+ const id = Buffer.from(credentials.id, 'base64');
125
+ const attestationObject = Buffer.from(credentials.response.attestationObject, 'base64');
126
+ const clientDataJSON = Buffer.from(credentials.response.clientDataJSON, 'base64');
127
+
128
+ const attestationResponse = {
129
+ id: toArrayBuffer(id),
130
+ response: {
131
+ attestationObject: toArrayBuffer(attestationObject),
132
+ clientDataJSON: toArrayBuffer(clientDataJSON),
133
+ },
134
+ };
135
+
136
+ const registrationOptions = await f2l.attestationResult(
137
+ attestationResponse,
138
+ attestationExpectations,
139
+ );
140
+
141
+ const publicKey = registrationOptions?.authnrData?.get('credentialPublicKeyPem');
142
+ const aaguidArrayBuffer = registrationOptions?.authnrData?.get('aaguid'); // ArrayBuffer Uint8...
143
+ const counter = registrationOptions?.authnrData?.get('counter');
144
+
145
+ const aaguidConcatenated = buf2hex(aaguidArrayBuffer);
146
+ const aaguid = `${aaguidConcatenated.slice(0, 8)}-${aaguidConcatenated.slice(
147
+ 8,
148
+ 12,
149
+ )}-${aaguidConcatenated.slice(12, 16)}-${aaguidConcatenated.slice(
150
+ 16,
151
+ 20,
152
+ )}-${aaguidConcatenated.slice(20)}`;
153
+
154
+ return { publicKey, counter, id: credentials.id, aaguid, created: new Date() };
155
+ },
156
+
157
+ verifyCredentialRequest: async (userPublicKeys, username, credentials) => {
158
+ const request = await WebAuthnCredentialsCreationRequests.findOne(
159
+ {
160
+ _id: credentials.requestId,
161
+ },
162
+ { sort: { _id: -1 } },
163
+ );
164
+
165
+ const id = Buffer.from(credentials.id, 'base64');
166
+ const authenticatorData = Buffer.from(credentials.response.authenticatorData, 'base64');
167
+ const signature = Buffer.from(credentials.response.signature, 'base64');
168
+ const userHandle = Buffer.from(credentials.response.userHandle, 'base64');
169
+ const clientDataJSON = Buffer.from(credentials.response.clientDataJSON, 'base64');
170
+
171
+ const { publicKey, counter } =
172
+ userPublicKeys.find((publicCredentials) => {
173
+ return credentials.id === publicCredentials.id;
174
+ }) || {};
175
+
176
+ if (!publicKey) throw new Error('WebAuthn not setup');
177
+
178
+ const assertionExpectations = {
179
+ challenge: request.challenge,
180
+ origin: request.origin,
181
+ factor: request.factor,
182
+ prevCounter: counter,
183
+ publicKey,
184
+ userHandle: toArrayBuffer(Buffer.from(username)),
185
+ };
186
+
187
+ const assertionResponse = {
188
+ id: toArrayBuffer(id),
189
+ response: {
190
+ authenticatorData: toArrayBuffer(authenticatorData),
191
+ clientDataJSON: toArrayBuffer(clientDataJSON),
192
+ signature: toArrayBuffer(signature),
193
+ userHandle: toArrayBuffer(userHandle),
194
+ },
195
+ };
196
+
197
+ const loginResult = await f2l.assertionResult(assertionResponse, assertionExpectations);
198
+ return { userHandle: loginResult?.authnrData?.get('userHandle') };
199
+ },
200
+ };
201
+ };
@@ -0,0 +1,39 @@
1
+ // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2
2
+ const PBKDF2_ITERATIONS = 300000; // Iterations, > 210'000
3
+ const PBKDF2_KEY_LENGTH = 256; // Bytes
4
+ const PBKDF2_SALT_LENGTH = 16; // Bytes
5
+
6
+ export function generateSalt(saltLength = PBKDF2_SALT_LENGTH) {
7
+ const array = new Uint32Array(saltLength);
8
+ return Buffer.from(crypto.getRandomValues(array)).toString('hex');
9
+ }
10
+
11
+ export async function getDerivedKey(
12
+ salt: string,
13
+ password: string,
14
+ iterations = PBKDF2_ITERATIONS,
15
+ keyLength = PBKDF2_KEY_LENGTH,
16
+ ) {
17
+ const textEncoder = new TextEncoder();
18
+ const passwordBuffer = textEncoder.encode(password);
19
+ const importedKey = await crypto.subtle.importKey('raw', passwordBuffer, 'PBKDF2', false, [
20
+ 'deriveBits',
21
+ ]);
22
+
23
+ const bits = await crypto.subtle.deriveBits(
24
+ {
25
+ name: 'PBKDF2',
26
+ hash: 'SHA-512',
27
+ salt: textEncoder.encode(salt),
28
+ iterations,
29
+ },
30
+ importedKey,
31
+ keyLength,
32
+ );
33
+ return Buffer.from(bits).toString('hex');
34
+ }
35
+
36
+ export async function compare(password: string, hash: string, salt: string) {
37
+ const comparableHash = await getDerivedKey(salt, password);
38
+ return comparableHash === hash;
39
+ }
@@ -0,0 +1,5 @@
1
+ export const hash = async (message) => {
2
+ const bytes = new TextEncoder().encode(message);
3
+ const hashBuffer = await crypto.subtle.digest('SHA-256', bytes);
4
+ return Buffer.from(hashBuffer).toString('hex');
5
+ };
@@ -0,0 +1,31 @@
1
+ import { MigrateUserDataService } from '@unchainedshop/types/user.js';
2
+ import { userSettings } from '../users-settings.js';
3
+
4
+ export const migrateUserDataService: MigrateUserDataService = async (
5
+ userIdBeforeLogin,
6
+ userId,
7
+ unchainedAPI,
8
+ ) => {
9
+ const user = await unchainedAPI.modules.users.findUserById(userId);
10
+ const userBeforeLogin = await unchainedAPI.modules.users.findUserById(userIdBeforeLogin);
11
+
12
+ await unchainedAPI.services.orders.migrateOrderCarts(
13
+ {
14
+ fromUserId: userIdBeforeLogin,
15
+ toUserId: userId,
16
+ shouldMerge: userSettings.mergeUserCartsOnLogin,
17
+ countryContext: userBeforeLogin.lastLogin?.countryCode || user.lastLogin?.countryCode,
18
+ },
19
+ unchainedAPI,
20
+ );
21
+
22
+ await unchainedAPI.services.bookmarks.migrateBookmarks(
23
+ {
24
+ fromUserId: userIdBeforeLogin,
25
+ toUserId: userId,
26
+ shouldMerge: userSettings.mergeUserCartsOnLogin,
27
+ countryContext: userBeforeLogin.lastLogin?.countryCode || user.lastLogin?.countryCode,
28
+ },
29
+ unchainedAPI,
30
+ );
31
+ };
@@ -1,10 +1,11 @@
1
- import { UserServices } from '@unchainedshop/types/user.js';
2
1
  import { getUserCountryService } from './getUserCountryService.js';
3
2
  import { getUserLanguageService } from './getUserLanguageService.js';
4
3
  import { updateUserAvatarAfterUploadService } from './updateUserAvatarAfterUploadService.js';
4
+ import { migrateUserDataService } from './migrateUserDataService.js';
5
5
 
6
- export const userServices: UserServices = {
6
+ export const userServices = {
7
7
  getUserCountry: getUserCountryService,
8
8
  getUserLanguage: getUserLanguageService,
9
9
  updateUserAvatarAfterUpload: updateUserAvatarAfterUploadService,
10
+ migrateUserData: migrateUserDataService,
10
11
  };
@@ -1,3 +1,4 @@
1
1
  export { configureUsersModule } from './module/configureUsersModule.js';
2
2
 
3
3
  export { userServices } from './services/userServices.js';
4
+ export { userSettings } from './users-settings.js';
@@ -0,0 +1,71 @@
1
+ import { User, UserSettings, UserSettingsOptions } from '@unchainedshop/types/user.js';
2
+ import { Schemas } from '@unchainedshop/utils';
3
+ import { mongodb } from '@unchainedshop/mongodb';
4
+
5
+ export const userSettings: UserSettings = {
6
+ autoMessagingAfterUserCreation: null,
7
+ mergeUserCartsOnLogin: null,
8
+ validateEmail: null,
9
+ validateUsername: null,
10
+ validateNewUser: null,
11
+ validatePassword: null,
12
+
13
+ configureSettings: (
14
+ {
15
+ mergeUserCartsOnLogin,
16
+ autoMessagingAfterUserCreation,
17
+ validateEmail,
18
+ validateUsername,
19
+ validateNewUser,
20
+ validatePassword,
21
+ }: UserSettingsOptions,
22
+ db: mongodb.Db,
23
+ ) => {
24
+ const defaultAutoMessagingAfterUserCreation = true;
25
+ const defaultMergeUserCartsOnLogin = true;
26
+
27
+ const defaultValidateEmail = async (rawEmail: string) => {
28
+ const email = rawEmail.toLowerCase().trim();
29
+ if (!email?.includes?.('@')) return false;
30
+ const emailAlreadyExists = await db
31
+ .collection('users')
32
+ .countDocuments({ 'emails.address': { $regex: email, $options: 'i' } }, { limit: 1 });
33
+ if (emailAlreadyExists) return false;
34
+ return true;
35
+ };
36
+ const defaultValidateUsername = async (rawUsername: string) => {
37
+ const username = rawUsername.toLowerCase().trim();
38
+ if (username?.length < 3) return false;
39
+ const usernameAlreadyExists = await db
40
+ .collection('users')
41
+ .countDocuments({ username: { $regex: username, $options: 'i' } }, { limit: 1 });
42
+ if (usernameAlreadyExists) return false;
43
+ return true;
44
+ };
45
+ const defaultValidateNewUser = async (user: User) => {
46
+ const customSchema = Schemas.User.omit(
47
+ '_id',
48
+ 'created',
49
+ 'roles',
50
+ 'emails',
51
+ 'services',
52
+ 'username',
53
+ 'initialPassword',
54
+ );
55
+ customSchema.validate(user);
56
+ return Schemas.User.clean(user) as User;
57
+ };
58
+
59
+ const defaultValidatePassword = async (password: string) => {
60
+ return password?.length >= 8;
61
+ };
62
+
63
+ userSettings.mergeUserCartsOnLogin = mergeUserCartsOnLogin ?? defaultMergeUserCartsOnLogin;
64
+ userSettings.autoMessagingAfterUserCreation =
65
+ autoMessagingAfterUserCreation ?? defaultAutoMessagingAfterUserCreation;
66
+ userSettings.validateEmail = validateEmail || defaultValidateEmail;
67
+ userSettings.validateUsername = validateUsername || defaultValidateUsername;
68
+ userSettings.validateNewUser = validateNewUser || defaultValidateNewUser;
69
+ userSettings.validatePassword = validatePassword || defaultValidatePassword;
70
+ },
71
+ };
@@ -37,14 +37,6 @@ export default {
37
37
  token: {
38
38
  secret: 'secret',
39
39
  },
40
- resume: {
41
- loginTokens: [
42
- {
43
- hashedToken: '12eK1jAbw9kcP5u/sha4M5T2pKVVQol1aAbITYGmtCI=',
44
- when: new Date('2022-11-27T10:36:00.313Z'),
45
- },
46
- ],
47
- },
48
40
  webAuthn: [],
49
41
  web3: [
50
42
  {