@unchainedshop/core-users 4.5.0 → 4.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.
package/README.md CHANGED
@@ -120,6 +120,37 @@ await usersModule.updateProfile(userId, {
120
120
  | `USER_UPDATE_ROLES` | Roles changed |
121
121
  | `USER_ACCOUNT_ACTION` | Account action triggered |
122
122
 
123
+ ## Security
124
+
125
+ This module implements security best practices for user authentication and data protection.
126
+
127
+ ### Password Security
128
+
129
+ - **Algorithm**: PBKDF2 with SHA-512
130
+ - **Iterations**: 300,000 (exceeds OWASP recommendation)
131
+ - **Salt**: 16 bytes, cryptographically random per password
132
+ - **Key Length**: 256 bytes
133
+ - **FIPS 140-3**: Compatible when running on FIPS-enabled Node.js
134
+
135
+ ### Token Security
136
+
137
+ - **Generation**: `crypto.randomUUID()` (CSPRNG-based)
138
+ - **Storage**: SHA-256 hashed before database storage
139
+ - **Expiration**: Time-limited (configurable, default 1 hour)
140
+ - **Single-use**: Tokens invalidated after verification
141
+
142
+ ### WebAuthn/FIDO2
143
+
144
+ Full support for passwordless authentication via hardware security keys and platform authenticators, providing phishing-resistant authentication.
145
+
146
+ ### Data Protection
147
+
148
+ - Sensitive data (password hashes, tokens) stripped from event emissions via `removeConfidentialServiceHashes()`
149
+ - Soft delete preserves audit trail while removing PII
150
+ - Email addresses and profile data access-controlled via RBAC
151
+
152
+ See [SECURITY.md](../../SECURITY.md) for complete security documentation.
153
+
123
154
  ## License
124
155
 
125
156
  EUPL-1.2
@@ -30,7 +30,7 @@ export interface Email {
30
30
  }
31
31
  export type User = {
32
32
  _id: string;
33
- deleted?: Date;
33
+ deleted?: Date | null;
34
34
  avatarId?: string;
35
35
  emails: Email[];
36
36
  guest: boolean;
@@ -6,7 +6,7 @@ export interface WebAuthnCredentialsCreationRequest {
6
6
  factor: 'first' | 'second' | 'either';
7
7
  }
8
8
  type Collection = WebAuthnCredentialsCreationRequest & {
9
- _id: number;
9
+ _id: string;
10
10
  };
11
11
  export declare const WebAuthnCredentialsCreationRequestsCollection: (db: mongodb.Db) => Promise<mongodb.Collection<Collection>>;
12
12
  export {};
@@ -33,7 +33,7 @@ export declare const configureUsersModule: (moduleInput: ModuleInput<UserSetting
33
33
  counter?: number;
34
34
  transports?: import("@passwordless-id/webauthn/dist/esm/types.js").ExtendedAuthenticatorTransport[];
35
35
  }[], username: string, credentials: import("@passwordless-id/webauthn/dist/esm/types.js").AuthenticationJSON & {
36
- requestId: number;
36
+ requestId: string;
37
37
  }) => Promise<{
38
38
  userHandle: string;
39
39
  counter: number;
@@ -95,6 +95,10 @@ export declare const configureUsersModule: (moduleInput: ModuleInput<UserSetting
95
95
  created: Date;
96
96
  }): Promise<User | null>;
97
97
  removeWebAuthnCredential(userId: string, credentialsId: string): Promise<User | null>;
98
+ createAccessToken(username: string): Promise<{
99
+ user: User;
100
+ token: string;
101
+ } | null>;
98
102
  setAccessToken(username: string, plainSecret: string): Promise<User | null>;
99
103
  verifyWeb3SignatureAndUpdate(user: User, credentials: {
100
104
  address: string;
@@ -189,7 +189,7 @@ export const configureUsersModule = async (moduleInput) => {
189
189
  }).toArray();
190
190
  },
191
191
  async userExists({ userId }) {
192
- const userCount = await Users.countDocuments({ _id: userId, deleted: { $exists: false } }, { limit: 1 });
192
+ const userCount = await Users.countDocuments({ _id: userId, deleted: null }, { limit: 1 });
193
193
  return userCount === 1;
194
194
  },
195
195
  primaryEmail(user) {
@@ -311,7 +311,7 @@ export const configureUsersModule = async (moduleInput) => {
311
311
  const existingEntry = user.services?.web3?.find((service) => service.address.toLowerCase() === address.toLowerCase());
312
312
  if (existingEntry)
313
313
  return user;
314
- const nonce = Math.floor(Math.random() * 1000000).toString();
314
+ const nonce = crypto.randomUUID();
315
315
  const updatedUser = await Users.findOneAndUpdate(generateDbFilterById(userId), {
316
316
  $push: {
317
317
  'services.web3': {
@@ -369,8 +369,20 @@ export const configureUsersModule = async (moduleInput) => {
369
369
  }, { returnDocument: 'after' });
370
370
  return updatedUser;
371
371
  },
372
+ async createAccessToken(username) {
373
+ const plainToken = crypto.randomUUID();
374
+ const secret = await sha256(plainToken);
375
+ const updatedUser = await Users.findOneAndUpdate({ username: insensitiveTrimmedRegexOperator(username) }, {
376
+ $set: {
377
+ 'services.token': { secret },
378
+ },
379
+ }, { returnDocument: 'after' });
380
+ if (!updatedUser)
381
+ return null;
382
+ return { user: updatedUser, token: plainToken };
383
+ },
372
384
  async setAccessToken(username, plainSecret) {
373
- const secret = await sha256(`${username}:${plainSecret}`);
385
+ const secret = await sha256(plainSecret);
374
386
  const updatedUser = await Users.findOneAndUpdate({ username: insensitiveTrimmedRegexOperator(username) }, {
375
387
  $set: {
376
388
  'services.token': { secret },
@@ -756,7 +768,7 @@ export const configureUsersModule = async (moduleInput) => {
756
768
  existingTags: async () => {
757
769
  const tags = (await Users.distinct('tags', {
758
770
  tags: { $exists: true },
759
- deleted: { $exists: false },
771
+ deleted: null,
760
772
  }));
761
773
  return tags.filter(Boolean).toSorted();
762
774
  },
@@ -1,11 +1,11 @@
1
- import type { ModuleInput } from '@unchainedshop/mongodb';
1
+ import { type ModuleInput } from '@unchainedshop/mongodb';
2
2
  import type { RegistrationJSON, AuthenticationJSON, CredentialInfo, NamedAlgo, ExtendedAuthenticatorTransport } from '@passwordless-id/webauthn/dist/esm/types.js';
3
3
  export type { RegistrationJSON, AuthenticationJSON, CredentialInfo, NamedAlgo, ExtendedAuthenticatorTransport, };
4
4
  export declare function toArrayBuffer(buffer: Buffer): ArrayBuffer;
5
5
  export declare function buf2hex(buffer: ArrayBuffer): string;
6
6
  export interface WebAuthnCredentialCreationOptions {
7
7
  challenge: string;
8
- requestId: number;
8
+ requestId: string;
9
9
  rp: {
10
10
  id: string;
11
11
  name: string;
@@ -29,7 +29,7 @@ export interface WebAuthnCredentialCreationOptions {
29
29
  }
30
30
  export interface WebAuthnCredentialRequestOptions {
31
31
  challenge: string;
32
- requestId: number;
32
+ requestId: string;
33
33
  rpId: string;
34
34
  timeout: number;
35
35
  userVerification?: 'required' | 'preferred' | 'discouraged';
@@ -75,10 +75,11 @@ export declare const configureUsersWebAuthnModule: ({ db }: ModuleInput<Record<s
75
75
  counter?: number;
76
76
  transports?: ExtendedAuthenticatorTransport[];
77
77
  }[], username: string, credentials: AuthenticationJSON & {
78
- requestId: number;
78
+ requestId: string;
79
79
  }) => Promise<{
80
80
  userHandle: string;
81
81
  counter: number;
82
82
  } | null>;
83
83
  deleteUserWebAuthnCredentials: (username: string) => Promise<number>;
84
84
  }>;
85
+ export type UsersWebAuthnModule = Awaited<ReturnType<typeof configureUsersWebAuthnModule>>;
@@ -1,3 +1,4 @@
1
+ import { generateDbObjectId } from '@unchainedshop/mongodb';
1
2
  import { createLogger } from '@unchainedshop/logger';
2
3
  import pMemoize from 'p-memoize';
3
4
  import ExpiryMap from 'expiry-map';
@@ -5,7 +6,6 @@ import { server as webauthnServer } from '@passwordless-id/webauthn';
5
6
  import { WebAuthnCredentialsCreationRequestsCollection } from "../db/WebAuthnCredentialsCreationRequestsCollection.js";
6
7
  const logger = createLogger('unchained:core-users');
7
8
  const ONE_DAY_MS = 24 * 60 * 60 * 1000;
8
- const { ROOT_URL = 'http://localhost:4010', EMAIL_WEBSITE_NAME = 'Unchained' } = process.env;
9
9
  async function fetchMDSEntriesImpl() {
10
10
  const cache = new Map();
11
11
  try {
@@ -48,6 +48,7 @@ export function buf2hex(buffer) {
48
48
  .join('');
49
49
  }
50
50
  export const configureUsersWebAuthnModule = async ({ db }) => {
51
+ const { ROOT_URL = 'http://localhost:4010', EMAIL_WEBSITE_NAME = 'Unchained' } = process.env;
51
52
  const WebAuthnCredentialsCreationRequests = await WebAuthnCredentialsCreationRequestsCollection(db);
52
53
  const thisDomain = new URL(ROOT_URL).hostname;
53
54
  const thisOrigin = new URL(ROOT_URL).origin;
@@ -60,7 +61,7 @@ export const configureUsersWebAuthnModule = async ({ db }) => {
60
61
  createCredentialCreationOptions: async (origin, username, extensionOptions) => {
61
62
  const challenge = webauthnServer.randomChallenge();
62
63
  const { insertedId } = await WebAuthnCredentialsCreationRequests.insertOne({
63
- _id: new Date().getTime(),
64
+ _id: generateDbObjectId(),
64
65
  challenge,
65
66
  origin,
66
67
  factor: 'either',
@@ -92,7 +93,7 @@ export const configureUsersWebAuthnModule = async ({ db }) => {
92
93
  createCredentialRequestOptions: async (origin, username, extensionOptions) => {
93
94
  const challenge = webauthnServer.randomChallenge();
94
95
  const { insertedId } = await WebAuthnCredentialsCreationRequests.insertOne({
95
- _id: new Date().getTime(),
96
+ _id: generateDbObjectId(),
96
97
  challenge,
97
98
  origin,
98
99
  factor: 'either',
@@ -1,3 +1,4 @@
1
+ import { timingSafeStringEqual } from '@unchainedshop/utils';
1
2
  const PBKDF2_ITERATIONS = 300000;
2
3
  const PBKDF2_KEY_LENGTH = 256;
3
4
  const PBKDF2_SALT_LENGTH = 16;
@@ -26,5 +27,5 @@ export async function getDerivedKey(salt, password, iterations = PBKDF2_ITERATIO
26
27
  }
27
28
  export async function compare(password, hash, salt) {
28
29
  const comparableHash = await getDerivedKey(salt, password);
29
- return comparableHash === hash;
30
+ return timingSafeStringEqual(comparableHash, hash);
30
31
  }
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@unchainedshop/core-users",
3
- "version": "4.5.0",
3
+ "description": "User management module for the Unchained Engine with authentication support",
4
+ "version": "4.6.0",
4
5
  "main": "lib/users-index.js",
5
6
  "types": "lib/users-index.d.ts",
6
7
  "type": "module",
@@ -35,12 +36,12 @@
35
36
  "homepage": "https://github.com/unchainedshop/unchained#readme",
36
37
  "dependencies": {
37
38
  "@passwordless-id/webauthn": "^2.3.1",
38
- "@unchainedshop/events": "^4.5.0",
39
- "@unchainedshop/file-upload": "^4.5.0",
40
- "@unchainedshop/logger": "^4.5.0",
41
- "@unchainedshop/mongodb": "^4.5.0",
42
- "@unchainedshop/roles": "^4.5.0",
43
- "@unchainedshop/utils": "^4.5.0",
39
+ "@unchainedshop/events": "^4.6.0",
40
+ "@unchainedshop/file-upload": "^4.6.0",
41
+ "@unchainedshop/logger": "^4.6.0",
42
+ "@unchainedshop/mongodb": "^4.6.0",
43
+ "@unchainedshop/roles": "^4.6.0",
44
+ "@unchainedshop/utils": "^4.6.0",
44
45
  "bcryptjs": "^3.0.2",
45
46
  "expiry-map": "^2.0.0",
46
47
  "p-memoize": "^8.0.0"