@furystack/security 7.0.9 → 8.0.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/CHANGELOG.md +10 -0
- package/README.md +9 -3
- package/esm/crypto-password-hasher.d.ts +10 -12
- package/esm/crypto-password-hasher.d.ts.map +1 -1
- package/esm/crypto-password-hasher.js +43 -36
- package/esm/crypto-password-hasher.js.map +1 -1
- package/esm/helpers.d.ts +20 -7
- package/esm/helpers.d.ts.map +1 -1
- package/esm/helpers.js +27 -13
- package/esm/helpers.js.map +1 -1
- package/esm/index.d.ts +3 -1
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +3 -1
- package/esm/index.js.map +1 -1
- package/esm/password-authenticator.d.ts +26 -15
- package/esm/password-authenticator.d.ts.map +1 -1
- package/esm/password-authenticator.js +73 -109
- package/esm/password-authenticator.js.map +1 -1
- package/esm/password-authenticator.spec.js +122 -212
- package/esm/password-authenticator.spec.js.map +1 -1
- package/esm/password-credential-store.d.ts +50 -0
- package/esm/password-credential-store.d.ts.map +1 -0
- package/esm/password-credential-store.js +73 -0
- package/esm/password-credential-store.js.map +1 -0
- package/esm/password-credential-store.spec.d.ts +2 -0
- package/esm/password-credential-store.spec.d.ts.map +1 -0
- package/esm/password-credential-store.spec.js +64 -0
- package/esm/password-credential-store.spec.js.map +1 -0
- package/esm/password-hasher.d.ts +21 -3
- package/esm/password-hasher.d.ts.map +1 -1
- package/esm/password-hasher.js +1 -19
- package/esm/password-hasher.js.map +1 -1
- package/esm/security-policy-manager.d.ts +46 -14
- package/esm/security-policy-manager.d.ts.map +1 -1
- package/esm/security-policy-manager.js +48 -52
- package/esm/security-policy-manager.js.map +1 -1
- package/esm/security-policy-manager.spec.js +58 -7
- package/esm/security-policy-manager.spec.js.map +1 -1
- package/esm/security-policy.d.ts +35 -9
- package/esm/security-policy.d.ts.map +1 -1
- package/esm/security-policy.js +23 -33
- package/esm/security-policy.js.map +1 -1
- package/package.json +5 -5
- package/src/crypto-password-hasher.ts +46 -39
- package/src/helpers.ts +27 -15
- package/src/index.ts +3 -2
- package/src/password-authenticator.spec.ts +134 -254
- package/src/password-authenticator.ts +92 -86
- package/src/password-credential-store.spec.ts +77 -0
- package/src/password-credential-store.ts +79 -0
- package/src/password-hasher.ts +21 -9
- package/src/security-policy-manager.spec.ts +63 -7
- package/src/security-policy-manager.ts +91 -42
- package/src/security-policy.ts +50 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [8.0.0] - 2026-04-25
|
|
4
|
+
|
|
5
|
+
### 💥 Breaking Changes
|
|
6
|
+
|
|
7
|
+
Password policy, hasher, and authenticator are now DI tokens, not `@Injectable` classes. See the [v7 migration guide](../../docs/migrations/v7-functional-di.md) for rationale, recipes, and pitfalls.
|
|
8
|
+
|
|
9
|
+
- Removed the `@Injectable`-based `PasswordAuthenticator` and `CryptoPasswordHasher` classes. Resolve them as tokens via `injector.get(PasswordAuthenticator)` / `injector.get(CryptoPasswordHasher)`.
|
|
10
|
+
- Added `SecurityPolicy` interface + token. `usePasswordPolicy(injector, overrides?)` binds the settings, the hasher defaults, and the authenticator token.
|
|
11
|
+
- `PasswordCredentialStore` and `PasswordResetTokenStore` are now throw-by-default `StoreToken`s. Apps must `injector.bind(…, factory)` a persistent implementation before resolving anything downstream.
|
|
12
|
+
|
|
3
13
|
## [7.0.9] - 2026-04-17
|
|
4
14
|
|
|
5
15
|
### ⬆️ Dependencies
|
package/README.md
CHANGED
|
@@ -15,10 +15,10 @@ yarn add @furystack/security
|
|
|
15
15
|
### Setup a password policy
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
import {
|
|
18
|
+
import { createInjector } from '@furystack/inject'
|
|
19
19
|
import { usePasswordPolicy, createMinLengthComplexityRule, createMaxLengthComplexityRule } from '@furystack/security'
|
|
20
20
|
|
|
21
|
-
const injector =
|
|
21
|
+
const injector = createInjector()
|
|
22
22
|
usePasswordPolicy(injector, {
|
|
23
23
|
passwordComplexityRules: [createMinLengthComplexityRule(8), createMaxLengthComplexityRule(64)],
|
|
24
24
|
passwordExpirationDays: 90,
|
|
@@ -26,12 +26,18 @@ usePasswordPolicy(injector, {
|
|
|
26
26
|
})
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
`usePasswordPolicy` binds the `SecurityPolicy` settings, the
|
|
30
|
+
`CryptoPasswordHasher` defaults, and the `PasswordAuthenticator` token.
|
|
31
|
+
Applications must also bind concrete stores for the
|
|
32
|
+
`PasswordCredentialStore` and `PasswordResetTokenStore` tokens (they
|
|
33
|
+
throw by default) before resolving anything that depends on them.
|
|
34
|
+
|
|
29
35
|
### Check if a user's password is valid
|
|
30
36
|
|
|
31
37
|
```ts
|
|
32
38
|
import { PasswordAuthenticator } from '@furystack/security'
|
|
33
39
|
|
|
34
|
-
const authenticator = injector.
|
|
40
|
+
const authenticator = injector.get(PasswordAuthenticator)
|
|
35
41
|
|
|
36
42
|
const userName = 'john_doe'
|
|
37
43
|
const plainPassword = 'SecureP@ssw0rd!'
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
+
import { type Token } from '@furystack/inject';
|
|
1
2
|
import type { PasswordHasher } from './password-hasher.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
creationDate: string;
|
|
12
|
-
}>;
|
|
13
|
-
}
|
|
3
|
+
/**
|
|
4
|
+
* Default PBKDF2-based {@link PasswordHasher} implementation. Uses SHA-512
|
|
5
|
+
* with 1000 iterations and a 64-byte derived key length.
|
|
6
|
+
*
|
|
7
|
+
* Swap this token in {@link SecurityPolicy.hasher} to replace the hashing
|
|
8
|
+
* algorithm — for example with a stronger Argon2 implementation or a
|
|
9
|
+
* delegated hashing service.
|
|
10
|
+
*/
|
|
11
|
+
export declare const CryptoPasswordHasher: Token<PasswordHasher, 'singleton'>;
|
|
14
12
|
//# sourceMappingURL=crypto-password-hasher.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto-password-hasher.d.ts","sourceRoot":"","sources":["../src/crypto-password-hasher.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"crypto-password-hasher.d.ts","sourceRoot":"","sources":["../src/crypto-password-hasher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAG7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAiB1D;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC,cAAc,EAAE,WAAW,CAsBlE,CAAA"}
|
|
@@ -1,39 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
import { defineService } from '@furystack/inject';
|
|
2
|
+
import { pbkdf2, randomBytes } from 'crypto';
|
|
3
|
+
const ITERATIONS = 1000;
|
|
4
|
+
const KEY_LENGTH = 64;
|
|
5
|
+
const SALT_BYTES = 16;
|
|
6
|
+
const deriveHash = (password, salt) => new Promise((resolve, reject) => {
|
|
7
|
+
pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, 'sha512', (err, key) => {
|
|
8
|
+
if (err) {
|
|
9
|
+
reject(err);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
resolve(key.toString('hex'));
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Default PBKDF2-based {@link PasswordHasher} implementation. Uses SHA-512
|
|
17
|
+
* with 1000 iterations and a 64-byte derived key length.
|
|
18
|
+
*
|
|
19
|
+
* Swap this token in {@link SecurityPolicy.hasher} to replace the hashing
|
|
20
|
+
* algorithm — for example with a stronger Argon2 implementation or a
|
|
21
|
+
* delegated hashing service.
|
|
22
|
+
*/
|
|
23
|
+
export const CryptoPasswordHasher = defineService({
|
|
24
|
+
name: 'furystack/security/CryptoPasswordHasher',
|
|
25
|
+
lifetime: 'singleton',
|
|
26
|
+
factory: () => ({
|
|
27
|
+
verifyCredential: async (password, { passwordHash, salt }) => {
|
|
28
|
+
const candidate = await deriveHash(password, salt);
|
|
29
|
+
if (candidate === passwordHash) {
|
|
30
|
+
return { isValid: true };
|
|
31
|
+
}
|
|
32
|
+
return { isValid: false, reason: 'badUsernameOrPassword' };
|
|
33
|
+
},
|
|
34
|
+
createCredential: async (userName, password) => {
|
|
35
|
+
const salt = randomBytes(SALT_BYTES).toString('hex');
|
|
36
|
+
const passwordHash = await deriveHash(password, salt);
|
|
15
37
|
return {
|
|
16
|
-
|
|
38
|
+
userName,
|
|
39
|
+
salt,
|
|
40
|
+
passwordHash,
|
|
41
|
+
creationDate: new Date().toISOString(),
|
|
17
42
|
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
reason: 'badUsernameOrPassword',
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
async createCredential(userName, password) {
|
|
25
|
-
const salt = randomBytes(16).toString('hex');
|
|
26
|
-
const passwordHash = await new Promise((resolve, reject) => pbkdf2(password, salt, this.iterations, this.keylen, 'sha512', (err, key) => err ? reject(err) : resolve(key.toString('hex'))));
|
|
27
|
-
return {
|
|
28
|
-
userName,
|
|
29
|
-
salt,
|
|
30
|
-
passwordHash,
|
|
31
|
-
creationDate: new Date().toISOString(),
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
CryptoPasswordHasher = __decorate([
|
|
36
|
-
Injectable({ lifetime: 'singleton' })
|
|
37
|
-
], CryptoPasswordHasher);
|
|
38
|
-
export { CryptoPasswordHasher };
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
39
46
|
//# sourceMappingURL=crypto-password-hasher.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto-password-hasher.js","sourceRoot":"","sources":["../src/crypto-password-hasher.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"crypto-password-hasher.js","sourceRoot":"","sources":["../src/crypto-password-hasher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAc,MAAM,mBAAmB,CAAA;AAC7D,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAA;AAI5C,MAAM,UAAU,GAAG,IAAI,CAAA;AACvB,MAAM,UAAU,GAAG,EAAE,CAAA;AACrB,MAAM,UAAU,GAAG,EAAE,CAAA;AAErB,MAAM,UAAU,GAAG,CAAC,QAAgB,EAAE,IAAY,EAAmB,EAAE,CACrE,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;IACtC,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QACpE,IAAI,GAAG,EAAE,CAAC;YACR,MAAM,CAAC,GAAG,CAAC,CAAA;YACX,OAAM;QACR,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAA;IAC9B,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEJ;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAuC,aAAa,CAAC;IACpF,IAAI,EAAE,yCAAyC;IAC/C,QAAQ,EAAE,WAAW;IACrB,OAAO,EAAE,GAAmB,EAAE,CAAC,CAAC;QAC9B,gBAAgB,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,EAAgC,EAAE;YACzF,MAAM,SAAS,GAAG,MAAM,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;YAClD,IAAI,SAAS,KAAK,YAAY,EAAE,CAAC;gBAC/B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;YAC1B,CAAC;YACD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,uBAAuB,EAAE,CAAA;QAC5D,CAAC;QACD,gBAAgB,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE;YAC7C,MAAM,IAAI,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;YACpD,MAAM,YAAY,GAAG,MAAM,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;YACrD,OAAO;gBACL,QAAQ;gBACR,IAAI;gBACJ,YAAY;gBACZ,YAAY,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACvC,CAAA;QACH,CAAC;KACF,CAAC;CACH,CAAC,CAAA"}
|
package/esm/helpers.d.ts
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import type { Injector } from '@furystack/inject';
|
|
2
2
|
import { SecurityPolicy } from './security-policy.js';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Rebinds the {@link SecurityPolicy} on the given injector with the default
|
|
5
|
+
* policy merged with `overrides`.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Apply this at application setup before resolving any service that reads
|
|
8
|
+
* the policy — {@link PasswordAuthenticator}, {@link SecurityPolicyManager}
|
|
9
|
+
* or direct `injector.get(SecurityPolicy)` callers. Because those services
|
|
10
|
+
* capture the policy at resolve time, calling `usePasswordPolicy` after
|
|
11
|
+
* they have been resolved has no effect unless you also invalidate them.
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
13
|
+
* **Prerequisite:** bind persistent implementations of
|
|
14
|
+
* `PasswordCredentialStore` and `PasswordResetTokenStore` before resolving
|
|
15
|
+
* {@link PasswordAuthenticator}. Their default factories throw on purpose
|
|
16
|
+
* so password data is never silently kept in-memory.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* usePasswordPolicy(injector, {
|
|
21
|
+
* passwordExpirationDays: 90,
|
|
22
|
+
* passwordComplexityRules: [createMinLengthComplexityRule(8)],
|
|
23
|
+
* })
|
|
24
|
+
* ```
|
|
12
25
|
*/
|
|
13
|
-
export declare const usePasswordPolicy: (injector: Injector,
|
|
26
|
+
export declare const usePasswordPolicy: (injector: Injector, overrides?: Partial<SecurityPolicy>) => void;
|
|
14
27
|
//# sourceMappingURL=helpers.d.ts.map
|
package/esm/helpers.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAGjD,OAAO,EAAyB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE5E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,QAAQ,EAAE,YAAY,OAAO,CAAC,cAAc,CAAC,KAAG,IAK3F,CAAA"}
|
package/esm/helpers.js
CHANGED
|
@@ -1,19 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PasswordAuthenticator } from './password-authenticator.js';
|
|
2
|
+
import { SecurityPolicyManager } from './security-policy-manager.js';
|
|
3
|
+
import { defaultSecurityPolicy, SecurityPolicy } from './security-policy.js';
|
|
2
4
|
/**
|
|
3
|
-
*
|
|
5
|
+
* Rebinds the {@link SecurityPolicy} on the given injector with the default
|
|
6
|
+
* policy merged with `overrides`.
|
|
4
7
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
+
* Apply this at application setup before resolving any service that reads
|
|
9
|
+
* the policy — {@link PasswordAuthenticator}, {@link SecurityPolicyManager}
|
|
10
|
+
* or direct `injector.get(SecurityPolicy)` callers. Because those services
|
|
11
|
+
* capture the policy at resolve time, calling `usePasswordPolicy` after
|
|
12
|
+
* they have been resolved has no effect unless you also invalidate them.
|
|
8
13
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
14
|
+
* **Prerequisite:** bind persistent implementations of
|
|
15
|
+
* `PasswordCredentialStore` and `PasswordResetTokenStore` before resolving
|
|
16
|
+
* {@link PasswordAuthenticator}. Their default factories throw on purpose
|
|
17
|
+
* so password data is never silently kept in-memory.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* usePasswordPolicy(injector, {
|
|
22
|
+
* passwordExpirationDays: 90,
|
|
23
|
+
* passwordComplexityRules: [createMinLengthComplexityRule(8)],
|
|
24
|
+
* })
|
|
25
|
+
* ```
|
|
11
26
|
*/
|
|
12
|
-
export const usePasswordPolicy = (injector,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
injector.setExplicitInstance(plainPolicy, SecurityPolicy);
|
|
27
|
+
export const usePasswordPolicy = (injector, overrides) => {
|
|
28
|
+
injector.bind(SecurityPolicy, () => ({ ...defaultSecurityPolicy(), ...overrides }));
|
|
29
|
+
injector.invalidate(SecurityPolicy);
|
|
30
|
+
injector.invalidate(SecurityPolicyManager);
|
|
31
|
+
injector.invalidate(PasswordAuthenticator);
|
|
18
32
|
};
|
|
19
33
|
//# sourceMappingURL=helpers.js.map
|
package/esm/helpers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAA;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAA;AACpE,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE5E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,QAAkB,EAAE,SAAmC,EAAQ,EAAE;IACjG,QAAQ,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,qBAAqB,EAAE,EAAE,GAAG,SAAS,EAAE,CAAC,CAAC,CAAA;IACnF,QAAQ,CAAC,UAAU,CAAC,cAAc,CAAC,CAAA;IACnC,QAAQ,CAAC,UAAU,CAAC,qBAAqB,CAAC,CAAA;IAC1C,QAAQ,CAAC,UAAU,CAAC,qBAAqB,CAAC,CAAA;AAC5C,CAAC,CAAA"}
|
package/esm/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export * from './crypto-password-hasher.js';
|
|
2
2
|
export * from './errors/index.js';
|
|
3
|
+
export * from './helpers.js';
|
|
3
4
|
export * from './models/index.js';
|
|
4
5
|
export * from './password-authenticator.js';
|
|
5
6
|
export * from './password-complexity-rules/index.js';
|
|
7
|
+
export * from './password-credential-store.js';
|
|
6
8
|
export * from './password-hasher.js';
|
|
7
9
|
export * from './security-policy-manager.js';
|
|
8
|
-
export * from './
|
|
10
|
+
export * from './security-policy.js';
|
|
9
11
|
//# sourceMappingURL=index.d.ts.map
|
package/esm/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,6BAA6B,CAAA;AAC3C,cAAc,mBAAmB,CAAA;AACjC,cAAc,mBAAmB,CAAA;AACjC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,sCAAsC,CAAA;AACpD,cAAc,sBAAsB,CAAA;AACpC,cAAc,8BAA8B,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,6BAA6B,CAAA;AAC3C,cAAc,mBAAmB,CAAA;AACjC,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA;AACjC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,sCAAsC,CAAA;AACpD,cAAc,gCAAgC,CAAA;AAC9C,cAAc,sBAAsB,CAAA;AACpC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,sBAAsB,CAAA"}
|
package/esm/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export * from './crypto-password-hasher.js';
|
|
2
2
|
export * from './errors/index.js';
|
|
3
|
+
export * from './helpers.js';
|
|
3
4
|
export * from './models/index.js';
|
|
4
5
|
export * from './password-authenticator.js';
|
|
5
6
|
export * from './password-complexity-rules/index.js';
|
|
7
|
+
export * from './password-credential-store.js';
|
|
6
8
|
export * from './password-hasher.js';
|
|
7
9
|
export * from './security-policy-manager.js';
|
|
8
|
-
export * from './
|
|
10
|
+
export * from './security-policy.js';
|
|
9
11
|
//# sourceMappingURL=index.js.map
|
package/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,6BAA6B,CAAA;AAC3C,cAAc,mBAAmB,CAAA;AACjC,cAAc,mBAAmB,CAAA;AACjC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,sCAAsC,CAAA;AACpD,cAAc,sBAAsB,CAAA;AACpC,cAAc,8BAA8B,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,6BAA6B,CAAA;AAC3C,cAAc,mBAAmB,CAAA;AACjC,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA;AACjC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,sCAAsC,CAAA;AACpD,cAAc,gCAAgC,CAAA;AAC9C,cAAc,sBAAsB,CAAA;AACpC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,sBAAsB,CAAA"}
|
|
@@ -1,30 +1,41 @@
|
|
|
1
|
+
import { type Token } from '@furystack/inject';
|
|
1
2
|
import type { PasswordCheckResult } from './models/index.js';
|
|
2
3
|
import type { PasswordHasher } from './password-hasher.js';
|
|
3
4
|
import { SecurityPolicyManager } from './security-policy-manager.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Authenticator surface: check stored credentials, rotate passwords and
|
|
7
|
+
* consume reset tokens. All writes are funneled through a system-identity
|
|
8
|
+
* scope so the underlying DataSets skip caller-scoped authorization.
|
|
9
|
+
*/
|
|
10
|
+
export interface PasswordAuthenticator {
|
|
11
|
+
/** The hasher bound by the active {@link SecurityPolicy}. Exposed for use in tests and migration helpers. */
|
|
8
12
|
readonly hasher: PasswordHasher;
|
|
13
|
+
/** The {@link SecurityPolicyManager} resolved at construction. */
|
|
14
|
+
readonly policyManager: SecurityPolicyManager;
|
|
9
15
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* @returns An object that contains an { isValid } value that indicates if the password is valid
|
|
16
|
+
* Verifies that the stored credential for `userName` matches
|
|
17
|
+
* `plainPassword` and honours the policy's expiration rules.
|
|
13
18
|
*/
|
|
14
19
|
checkPasswordForUser(userName: string, plainPassword: string): Promise<PasswordCheckResult>;
|
|
15
20
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* @param plainPassword The password as a plain string
|
|
21
|
+
* Replaces the stored credential for `userName` with one derived from
|
|
22
|
+
* `plainPassword`. Requires the current `lastPassword` to be valid and
|
|
23
|
+
* `plainPassword` to pass the configured complexity rules.
|
|
20
24
|
*/
|
|
21
25
|
setPasswordForUser(userName: string, lastPassword: string, plainPassword: string): Promise<void>;
|
|
22
26
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
27
|
+
* Consumes `resetToken` and rotates the associated user's password to
|
|
28
|
+
* one derived from `plainPassword`. Expired tokens are rejected and
|
|
29
|
+
* cleaned up; bad tokens raise {@link UnauthenticatedError}.
|
|
26
30
|
*/
|
|
27
31
|
resetPasswordForUser(resetToken: string, plainPassword: string): Promise<void>;
|
|
28
|
-
policyManager: SecurityPolicyManager;
|
|
29
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* DI token for the singleton {@link PasswordAuthenticator}.
|
|
35
|
+
*
|
|
36
|
+
* The factory creates a system-identity child scope once (at first
|
|
37
|
+
* resolution) and uses it for every credential mutation. The scope is
|
|
38
|
+
* torn down alongside the owning injector via `onDispose`.
|
|
39
|
+
*/
|
|
40
|
+
export declare const PasswordAuthenticator: Token<PasswordAuthenticator, 'singleton'>;
|
|
30
41
|
//# sourceMappingURL=password-authenticator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"password-authenticator.d.ts","sourceRoot":"","sources":["../src/password-authenticator.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"password-authenticator.d.ts","sourceRoot":"","sources":["../src/password-authenticator.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,KAAK,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAG7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAA;AAEpE;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,6GAA6G;IAC7G,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAA;IAC7C;;;OAGG;IACH,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAA;IAC3F;;;;OAIG;IACH,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChG;;;;OAIG;IACH,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/E;AAED;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,qBAAqB,EAAE,WAAW,CAoE1E,CAAA"}
|
|
@@ -1,114 +1,78 @@
|
|
|
1
|
-
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
-
};
|
|
7
|
-
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
-
};
|
|
10
1
|
import { useSystemIdentityContext } from '@furystack/core';
|
|
11
|
-
import {
|
|
12
|
-
import { getDataSetFor } from '@furystack/repository';
|
|
2
|
+
import { defineService } from '@furystack/inject';
|
|
13
3
|
import { UnauthenticatedError } from './errors/index.js';
|
|
14
4
|
import { PasswordComplexityError } from './errors/password-complexity-error.js';
|
|
15
|
-
import {
|
|
5
|
+
import { PasswordCredentialDataSet, PasswordResetTokenDataSet } from './password-credential-store.js';
|
|
16
6
|
import { SecurityPolicyManager } from './security-policy-manager.js';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
await
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
await
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
__decorate([
|
|
89
|
-
Injected((injector) => useSystemIdentityContext({ injector, username: 'PasswordAuthenticator' })),
|
|
90
|
-
__metadata("design:type", Function)
|
|
91
|
-
], PasswordAuthenticator.prototype, "systemInjector", void 0);
|
|
92
|
-
__decorate([
|
|
93
|
-
Injected((injector) => getDataSetFor(injector, PasswordCredential, 'userName')),
|
|
94
|
-
__metadata("design:type", Function)
|
|
95
|
-
], PasswordAuthenticator.prototype, "passwordDataSet", void 0);
|
|
96
|
-
__decorate([
|
|
97
|
-
Injected((injector) => getDataSetFor(injector, PasswordResetToken, 'token')),
|
|
98
|
-
__metadata("design:type", Function)
|
|
99
|
-
], PasswordAuthenticator.prototype, "tokenDataSet", void 0);
|
|
100
|
-
__decorate([
|
|
101
|
-
Injected(function (injector) {
|
|
102
|
-
return injector.getInstance(this.policyManager.policy.hasher);
|
|
103
|
-
}),
|
|
104
|
-
__metadata("design:type", Function)
|
|
105
|
-
], PasswordAuthenticator.prototype, "hasher", void 0);
|
|
106
|
-
__decorate([
|
|
107
|
-
Injected(SecurityPolicyManager),
|
|
108
|
-
__metadata("design:type", SecurityPolicyManager)
|
|
109
|
-
], PasswordAuthenticator.prototype, "policyManager", void 0);
|
|
110
|
-
PasswordAuthenticator = __decorate([
|
|
111
|
-
Injectable({ lifetime: 'singleton' })
|
|
112
|
-
], PasswordAuthenticator);
|
|
113
|
-
export { PasswordAuthenticator };
|
|
7
|
+
/**
|
|
8
|
+
* DI token for the singleton {@link PasswordAuthenticator}.
|
|
9
|
+
*
|
|
10
|
+
* The factory creates a system-identity child scope once (at first
|
|
11
|
+
* resolution) and uses it for every credential mutation. The scope is
|
|
12
|
+
* torn down alongside the owning injector via `onDispose`.
|
|
13
|
+
*/
|
|
14
|
+
export const PasswordAuthenticator = defineService({
|
|
15
|
+
name: 'furystack/security/PasswordAuthenticator',
|
|
16
|
+
lifetime: 'singleton',
|
|
17
|
+
factory: ({ inject, injector, onDispose }) => {
|
|
18
|
+
const policyManager = inject(SecurityPolicyManager);
|
|
19
|
+
const hasher = inject(policyManager.policy.hasher);
|
|
20
|
+
const passwordDataSet = inject(PasswordCredentialDataSet);
|
|
21
|
+
const tokenDataSet = inject(PasswordResetTokenDataSet);
|
|
22
|
+
const systemInjector = useSystemIdentityContext({ injector, username: 'PasswordAuthenticator' });
|
|
23
|
+
onDispose(() => systemInjector[Symbol.asyncDispose]());
|
|
24
|
+
const checkPasswordForUser = async (userName, plainPassword) => {
|
|
25
|
+
const entry = await passwordDataSet.get(systemInjector, userName);
|
|
26
|
+
if (!entry) {
|
|
27
|
+
return { isValid: false, reason: 'badUsernameOrPassword' };
|
|
28
|
+
}
|
|
29
|
+
const result = await hasher.verifyCredential(plainPassword, entry);
|
|
30
|
+
if (result.isValid && policyManager.hasPasswordExpired(entry)) {
|
|
31
|
+
return { isValid: false, reason: 'passwordExpired' };
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
const replaceCredential = async (userName, plainPassword) => {
|
|
36
|
+
const newCredential = await hasher.createCredential(userName, plainPassword);
|
|
37
|
+
const existing = await passwordDataSet.get(systemInjector, userName);
|
|
38
|
+
if (existing) {
|
|
39
|
+
await passwordDataSet.remove(systemInjector, existing.userName);
|
|
40
|
+
}
|
|
41
|
+
await passwordDataSet.add(systemInjector, newCredential);
|
|
42
|
+
};
|
|
43
|
+
const setPasswordForUser = async (userName, lastPassword, plainPassword) => {
|
|
44
|
+
const complexityResult = await policyManager.matchPasswordComplexityRules(plainPassword);
|
|
45
|
+
if (!complexityResult.match) {
|
|
46
|
+
throw new PasswordComplexityError(complexityResult.errors);
|
|
47
|
+
}
|
|
48
|
+
const lastResult = await checkPasswordForUser(userName, lastPassword);
|
|
49
|
+
if (!lastResult.isValid) {
|
|
50
|
+
throw new UnauthenticatedError();
|
|
51
|
+
}
|
|
52
|
+
await replaceCredential(userName, plainPassword);
|
|
53
|
+
};
|
|
54
|
+
const resetPasswordForUser = async (resetToken, plainPassword) => {
|
|
55
|
+
const token = await tokenDataSet.get(systemInjector, resetToken);
|
|
56
|
+
if (!token) {
|
|
57
|
+
throw new UnauthenticatedError();
|
|
58
|
+
}
|
|
59
|
+
if (policyManager.hasTokenExpired(token)) {
|
|
60
|
+
await tokenDataSet.remove(systemInjector, resetToken);
|
|
61
|
+
throw new UnauthenticatedError();
|
|
62
|
+
}
|
|
63
|
+
const complexityResult = await policyManager.matchPasswordComplexityRules(plainPassword);
|
|
64
|
+
if (!complexityResult.match) {
|
|
65
|
+
throw new PasswordComplexityError(complexityResult.errors);
|
|
66
|
+
}
|
|
67
|
+
await replaceCredential(token.userName, plainPassword);
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
hasher,
|
|
71
|
+
policyManager,
|
|
72
|
+
checkPasswordForUser,
|
|
73
|
+
setPasswordForUser,
|
|
74
|
+
resetPasswordForUser,
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
});
|
|
114
78
|
//# sourceMappingURL=password-authenticator.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"password-authenticator.js","sourceRoot":"","sources":["../src/password-authenticator.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"password-authenticator.js","sourceRoot":"","sources":["../src/password-authenticator.ts"],"names":[],"mappings":"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"}
|