@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 +31 -0
- package/lib/db/UsersCollection.d.ts +1 -1
- package/lib/db/WebAuthnCredentialsCreationRequestsCollection.d.ts +1 -1
- package/lib/module/configureUsersModule.d.ts +5 -1
- package/lib/module/configureUsersModule.js +16 -4
- package/lib/module/configureUsersWebAuthnModule.d.ts +5 -4
- package/lib/module/configureUsersWebAuthnModule.js +4 -3
- package/lib/module/pbkdf2.js +2 -1
- package/package.json +8 -7
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
|
|
@@ -6,7 +6,7 @@ export interface WebAuthnCredentialsCreationRequest {
|
|
|
6
6
|
factor: 'first' | 'second' | 'either';
|
|
7
7
|
}
|
|
8
8
|
type Collection = WebAuthnCredentialsCreationRequest & {
|
|
9
|
-
_id:
|
|
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:
|
|
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:
|
|
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 =
|
|
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(
|
|
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:
|
|
771
|
+
deleted: null,
|
|
760
772
|
}));
|
|
761
773
|
return tags.filter(Boolean).toSorted();
|
|
762
774
|
},
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
96
|
+
_id: generateDbObjectId(),
|
|
96
97
|
challenge,
|
|
97
98
|
origin,
|
|
98
99
|
factor: 'either',
|
package/lib/module/pbkdf2.js
CHANGED
|
@@ -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
|
|
30
|
+
return timingSafeStringEqual(comparableHash, hash);
|
|
30
31
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unchainedshop/core-users",
|
|
3
|
-
"
|
|
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.
|
|
39
|
-
"@unchainedshop/file-upload": "^4.
|
|
40
|
-
"@unchainedshop/logger": "^4.
|
|
41
|
-
"@unchainedshop/mongodb": "^4.
|
|
42
|
-
"@unchainedshop/roles": "^4.
|
|
43
|
-
"@unchainedshop/utils": "^4.
|
|
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"
|